diff options
Diffstat (limited to 'rpki/publication.py')
-rw-r--r-- | rpki/publication.py | 408 |
1 files changed, 68 insertions, 340 deletions
diff --git a/rpki/publication.py b/rpki/publication.py index 5fc7f3dd..ca7f7792 100644 --- a/rpki/publication.py +++ b/rpki/publication.py @@ -1,35 +1,24 @@ # $Id$ # -# Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") -# -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# +# Copyright (C) 2013--2014 Dragon Research Labs ("DRL") +# Portions copyright (C) 2009--2012 Internet Systems Consortium ("ISC") # Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. +# copyright notices and this permission notice appear in all copies. # -# THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. +# THE SOFTWARE IS PROVIDED "AS IS" AND DRL, ISC, AND ARIN DISCLAIM ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DRL, +# ISC, OR ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ -RPKI "publication" protocol. +RPKI publication protocol. """ import os @@ -48,322 +37,86 @@ import rpki.log logger = logging.getLogger(__name__) -class publication_namespace(object): - """ - XML namespace parameters for publication protocol. - """ +class publication_namespace(object): xmlns = rpki.relaxng.publication.xmlns nsmap = rpki.relaxng.publication.nsmap -class control_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, publication_namespace): - """ - Virtual class for control channel objects. - """ - def serve_dispatch(self, r_msg, cb, eb): - """ - Action dispatch handler. This needs special handling because we - need to make sure that this PDU arrived via the control channel. - """ - if self.client is not None: - raise rpki.exceptions.BadQuery("Control query received on client channel") - rpki.xml_utils.data_elt.serve_dispatch(self, r_msg, cb, eb) - -class config_elt(control_elt): +class base_publication_elt(rpki.xml_utils.base_elt, publication_namespace): """ - <config/> element. This is a little weird because there should - never be more than one row in the SQL config table, but we have to - put the BPKI CRL somewhere and SQL is the least bad place available. - - So we reuse a lot of the SQL machinery, but we nail config_id at 1, - we don't expose it in the XML protocol, and we only support the get - and set actions. + Base element for publication protocol. Publish and withdraw PDUs subclass this. """ - attributes = ("action", "tag") - element_name = "config" - elements = ("bpki_crl",) + attributes = ("tag", "uri", "hash") - sql_template = rpki.sql.template( - "config", - "config_id", - ("bpki_crl", rpki.x509.CRL)) + tag = None + uri = None + der = None + hash = None + + _payload = None - wired_in_config_id = 1 - - def startElement(self, stack, name, attrs): - """ - StartElement() handler for config object. This requires special - handling because of the weird way we treat config_id. - """ - control_elt.startElement(self, stack, name, attrs) - self.config_id = self.wired_in_config_id + def __repr__(self): + return rpki.log.log_repr(self, self.tag, self.uri, self.hash, self.payload) - @classmethod - def fetch(cls, gctx): - """ - Fetch the config object from SQL. This requires special handling - because of the weird way we treat config_id. - """ - return cls.sql_fetch(gctx, cls.wired_in_config_id) - - def serve_set(self, r_msg, cb, eb): - """ - Handle a set action. This requires special handling because - config doesn't support the create method. - """ - if self.sql_fetch(self.gctx, self.config_id) is None: - control_elt.serve_create(self, r_msg, cb, eb) - else: - control_elt.serve_set(self, r_msg, cb, eb) + @property + def payload(self): + if self._payload is None and self.der is not None: + self._payload = rpki.x509.uri_dispatch(self.uri)(DER = self.der) + return self._payload - def serve_fetch_one_maybe(self): - """ - Find the config object on which a get or set method should - operate. - """ - return self.sql_fetch(self.gctx, self.config_id) - -class client_elt(control_elt): - """ - <client/> element. - """ - - element_name = "client" - attributes = ("action", "tag", "client_handle", "base_uri") - elements = ("bpki_cert", "bpki_glue") - booleans = ("clear_replay_protection",) - - sql_template = rpki.sql.template( - "client", - "client_id", - "client_handle", - "base_uri", - ("bpki_cert", rpki.x509.X509), - ("bpki_glue", rpki.x509.X509), - ("last_cms_timestamp", rpki.sundial.datetime)) - - base_uri = None - bpki_cert = None - bpki_glue = None - last_cms_timestamp = None - - def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb): - """ - Extra server actions for client_elt. - """ - actions = [] - if q_pdu.clear_replay_protection: - actions.append(self.serve_clear_replay_protection) - def loop(iterator, action): - action(iterator, eb) - rpki.async.iterator(actions, loop, cb) - - def serve_clear_replay_protection(self, cb, eb): - """ - Handle a clear_replay_protection action for this client. - """ - self.last_cms_timestamp = None - self.sql_mark_dirty() - cb() - - def serve_fetch_one_maybe(self): + def raise_if_error(self): """ - Find the client object on which a get, set, or destroy method - should operate, or which would conflict with a create method. + No-op unless this is a <report_error/> PDU. """ - return self.sql_fetch_where1(self.gctx, "client_handle = %s", (self.client_handle,)) - def serve_fetch_all(self): - """ - Find client objects on which a list method should operate. - """ - return self.sql_fetch_all(self.gctx) + pass - def check_allowed_uri(self, uri): - """ - Make sure that a target URI is within this client's allowed URI space. - """ - if not uri.startswith(self.base_uri): - raise rpki.exceptions.ForbiddenURI -class publication_object_elt(rpki.xml_utils.base_elt, publication_namespace): +class publish_elt(base_publication_elt): """ - Virtual class for publishable objects. These have very similar - syntax, differences lie in underlying datatype and methods. XML - methods are a little different from the pattern used for objects - that support the create/set/get/list/destroy actions, but - publishable objects don't go in SQL either so these classes would be - different in any case. + <publish/> element. """ - attributes = ("action", "tag", "client_handle", "uri") - payload_type = None - payload = None + element_name = "publish" def endElement(self, stack, name, text): """ - Handle a publishable element element. + Handle reading of the object to be published """ + assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack) if text: - self.payload = self.payload_type(Base64 = text) # pylint: disable=E1102 + self.der = text.decode("base64") stack.pop() def toXML(self): """ Generate XML element for publishable object. """ + elt = self.make_elt() - if self.payload: - elt.text = self.payload.get_Base64() + if self.der is not None: + elt.text = self.der.encode("base64") return elt - def serve_dispatch(self, r_msg, cb, eb): - """ - Action dispatch handler. - """ - # pylint: disable=E0203 - try: - if self.client is None: - raise rpki.exceptions.BadQuery("Client query received on control channel") - dispatch = { "publish" : self.serve_publish, - "withdraw" : self.serve_withdraw } - if self.action not in dispatch: - raise rpki.exceptions.BadQuery("Unexpected query: action %s" % self.action) - self.client.check_allowed_uri(self.uri) - dispatch[self.action]() - r_pdu = self.__class__() - r_pdu.action = self.action - r_pdu.tag = self.tag - r_pdu.uri = self.uri - r_msg.append(r_pdu) - cb() - except rpki.exceptions.NoObjectAtURI, e: - # This can happen when we're cleaning up from a prior mess, so - # we generate a <report_error/> PDU then carry on. - r_msg.append(report_error_elt.from_exception(e, self.tag)) - cb() - - def serve_publish(self): - """ - Publish an object. - """ - logger.info("Publishing %s", self.payload.tracking_data(self.uri)) - filename = self.uri_to_filename() - filename_tmp = filename + ".tmp" - dirname = os.path.dirname(filename) - if not os.path.isdir(dirname): - os.makedirs(dirname) - f = open(filename_tmp, "wb") - f.write(self.payload.get_DER()) - f.close() - os.rename(filename_tmp, filename) - - def serve_withdraw(self): - """ - Withdraw an object, then recursively delete empty directories. - """ - logger.info("Withdrawing %s", self.uri) - filename = self.uri_to_filename() - try: - os.remove(filename) - except OSError, e: - if e.errno == errno.ENOENT: - raise rpki.exceptions.NoObjectAtURI("No object published at %s" % self.uri) - else: - raise - min_path_len = len(self.gctx.publication_base.rstrip("/")) - dirname = os.path.dirname(filename) - while len(dirname) > min_path_len: - try: - os.rmdir(dirname) - except OSError: - break - else: - dirname = os.path.dirname(dirname) - - def uri_to_filename(self): - """ - Convert a URI to a local filename. - """ - if not self.uri.startswith("rsync://"): - raise rpki.exceptions.BadURISyntax(self.uri) - path = self.uri.split("/")[3:] - if not self.gctx.publication_multimodule: - del path[0] - path.insert(0, self.gctx.publication_base.rstrip("/")) - filename = "/".join(path) - if "/../" in filename or filename.endswith("/.."): - raise rpki.exceptions.BadURISyntax(filename) - return filename - - @classmethod - def make_publish(cls, uri, obj, tag = None): - """ - Construct a publication PDU. - """ - assert cls.payload_type is not None and type(obj) is cls.payload_type - return cls.make_pdu(action = "publish", uri = uri, payload = obj, tag = tag) - - @classmethod - def make_withdraw(cls, uri, obj, tag = None): - """ - Construct a withdrawal PDU. - """ - assert cls.payload_type is not None and type(obj) is cls.payload_type - return cls.make_pdu(action = "withdraw", uri = uri, tag = tag) - - def raise_if_error(self): - """ - No-op, since this is not a <report_error/> PDU. - """ - pass - -class certificate_elt(publication_object_elt): - """ - <certificate/> element. - """ - - element_name = "certificate" - payload_type = rpki.x509.X509 - -class crl_elt(publication_object_elt): - """ - <crl/> element. - """ - - element_name = "crl" - payload_type = rpki.x509.CRL -class manifest_elt(publication_object_elt): +class withdraw_elt(base_publication_elt): """ - <manifest/> element. + <withdraw/> element. """ - element_name = "manifest" - payload_type = rpki.x509.SignedManifest - -class roa_elt(publication_object_elt): - """ - <roa/> element. - """ + element_name = "withdraw" - element_name = "roa" - payload_type = rpki.x509.ROA -class ghostbuster_elt(publication_object_elt): +class list_elt(base_publication_elt): """ - <ghostbuster/> element. + <list/> element. """ - element_name = "ghostbuster" - payload_type = rpki.x509.Ghostbuster + element_name = "list" -publication_object_elt.obj2elt = dict( - (e.payload_type, e) for e in - (certificate_elt, crl_elt, manifest_elt, roa_elt, ghostbuster_elt)) class report_error_elt(rpki.xml_utils.text_elt, publication_namespace): """ @@ -374,18 +127,11 @@ class report_error_elt(rpki.xml_utils.text_elt, publication_namespace): attributes = ("tag", "error_code") text_attribute = "error_text" + error_code = None error_text = None - @classmethod - def from_exception(cls, e, tag = None): - """ - Generate a <report_error/> element from an exception. - """ - self = cls() - self.tag = tag - self.error_code = e.__class__.__name__ - self.error_text = str(e) - return self + def __repr__(self): + return rpki.log.log_repr(self, self.error_code, self.error_text) def __str__(self): s = "" @@ -400,11 +146,15 @@ class report_error_elt(rpki.xml_utils.text_elt, publication_namespace): """ Raise exception associated with this <report_error/> PDU. """ - t = rpki.exceptions.__dict__.get(self.error_code) - if isinstance(t, type) and issubclass(t, rpki.exceptions.RPKI_Exception): - raise t(getattr(self, "text", None)) - else: - raise rpki.exceptions.BadPublicationReply("Unexpected response from pubd: %s" % self) + + try: + e = getattr(rpki.exceptions, self.error_code) + if issubclass(e, rpki.exceptions.RPKI_Exception): + raise e(getattr(self, "text", None)) + except (TypeError, AttributeError): + pass + raise rpki.exceptions.BadPublicationReply("Unexpected response from pubd: %s" % self) + class msg(rpki.xml_utils.msg, publication_namespace): """ @@ -417,38 +167,8 @@ class msg(rpki.xml_utils.msg, publication_namespace): ## @var pdus # Dispatch table of PDUs for this protocol. - pdus = dict((x.element_name, x) for x in - (config_elt, client_elt, certificate_elt, crl_elt, manifest_elt, roa_elt, ghostbuster_elt, report_error_elt)) + pdus = dict((x.element_name, x) for x in (publish_elt, withdraw_elt, list_elt, report_error_elt)) - def serve_top_level(self, gctx, client, cb): - """ - Serve one msg PDU. - """ - if not self.is_query(): - raise rpki.exceptions.BadQuery("Message type is not query") - r_msg = self.__class__.reply() - - def loop(iterator, q_pdu): - - def fail(e): - if not isinstance(e, rpki.exceptions.NotFound): - logger.exception("Exception processing PDU %r", q_pdu) - r_msg.append(report_error_elt.from_exception(e, q_pdu.tag)) - cb(r_msg) - - try: - q_pdu.gctx = gctx - q_pdu.client = client - q_pdu.serve_dispatch(r_msg, iterator, fail) - except (rpki.async.ExitNow, SystemExit): - raise - except Exception, e: - fail(e) - - def done(): - cb(r_msg) - - rpki.async.iterator(self, loop, done) class sax_handler(rpki.xml_utils.sax_handler): """ @@ -468,3 +188,11 @@ class cms_msg(rpki.x509.XML_CMS_object): encoding = "us-ascii" schema = rpki.relaxng.publication saxify = sax_handler.saxify + +class cms_msg_no_sax(cms_msg): + """ + Transition kludge: varient of cms_msg (q.v.) with SAX parsing disabled. + If and when we ditch SAX entirely, this will become cms_msg. + """ + + saxify = None |