00001 """
00002 RPKI "publication" protocol.
00003
00004 $Id: publication.py 3449 2010-09-16 21:30:30Z sra $
00005
00006 Copyright (C) 2009--2010 Internet Systems Consortium ("ISC")
00007
00008 Permission to use, copy, modify, and distribute this software for any
00009 purpose with or without fee is hereby granted, provided that the above
00010 copyright notice and this permission notice appear in all copies.
00011
00012 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
00013 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00014 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
00015 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00016 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00017 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00018 PERFORMANCE OF THIS SOFTWARE.
00019
00020 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00021
00022 Permission to use, copy, modify, and distribute this software for any
00023 purpose with or without fee is hereby granted, provided that the above
00024 copyright notice and this permission notice appear in all copies.
00025
00026 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00027 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00028 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00029 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00030 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00031 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00032 PERFORMANCE OF THIS SOFTWARE.
00033 """
00034
00035 import base64, os, errno
00036 import rpki.resource_set, rpki.x509, rpki.sql, rpki.exceptions, rpki.xml_utils
00037 import rpki.http, rpki.up_down, rpki.relaxng, rpki.sundial, rpki.log, rpki.roa
00038
00039 class publication_namespace(object):
00040 """
00041 XML namespace parameters for publication protocol.
00042 """
00043
00044 xmlns = "http://www.hactrn.net/uris/rpki/publication-spec/"
00045 nsmap = { None : xmlns }
00046
00047 class control_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, publication_namespace):
00048 """
00049 Virtual class for control channel objects.
00050 """
00051
00052 def serve_dispatch(self, r_msg, cb, eb):
00053 """
00054 Action dispatch handler. This needs special handling because we
00055 need to make sure that this PDU arrived via the control channel.
00056 """
00057 if self.client is not None:
00058 raise rpki.exceptions.BadQuery, "Control query received on client channel"
00059 rpki.xml_utils.data_elt.serve_dispatch(self, r_msg, cb, eb)
00060
00061 class config_elt(control_elt):
00062 """
00063 <config/> element. This is a little weird because there should
00064 never be more than one row in the SQL config table, but we have to
00065 put the BPKI CRL somewhere and SQL is the least bad place available.
00066
00067 So we reuse a lot of the SQL machinery, but we nail config_id at 1,
00068 we don't expose it in the XML protocol, and we only support the get
00069 and set actions.
00070 """
00071
00072 attributes = ("action", "tag")
00073 element_name = "config"
00074 elements = ("bpki_crl",)
00075
00076 sql_template = rpki.sql.template("config", "config_id", ("bpki_crl", rpki.x509.CRL))
00077
00078 wired_in_config_id = 1
00079
00080 def startElement(self, stack, name, attrs):
00081 """
00082 StartElement() handler for config object. This requires special
00083 handling because of the weird way we treat config_id.
00084 """
00085 control_elt.startElement(self, stack, name, attrs)
00086 self.config_id = self.wired_in_config_id
00087
00088 @classmethod
00089 def fetch(cls, gctx):
00090 """
00091 Fetch the config object from SQL. This requires special handling
00092 because of the weird way we treat config_id.
00093 """
00094 return cls.sql_fetch(gctx, cls.wired_in_config_id)
00095
00096 def serve_set(self, r_msg, cb, eb):
00097 """
00098 Handle a set action. This requires special handling because
00099 config doesn't support the create method.
00100 """
00101 if self.sql_fetch(self.gctx, self.config_id) is None:
00102 control_elt.serve_create(self, r_msg, cb, eb)
00103 else:
00104 control_elt.serve_set(self, r_msg, cb, eb)
00105
00106 def serve_fetch_one_maybe(self):
00107 """
00108 Find the config object on which a get or set method should
00109 operate.
00110 """
00111 return self.sql_fetch(self.gctx, self.config_id)
00112
00113 class client_elt(control_elt):
00114 """
00115 <client/> element.
00116 """
00117
00118 element_name = "client"
00119 attributes = ("action", "tag", "client_handle", "base_uri")
00120 elements = ("bpki_cert", "bpki_glue")
00121
00122 sql_template = rpki.sql.template("client", "client_id", "client_handle", "base_uri",
00123 ("bpki_cert", rpki.x509.X509),
00124 ("bpki_glue", rpki.x509.X509),
00125 ("last_cms_timestamp", rpki.sundial.datetime))
00126
00127 base_uri = None
00128 bpki_cert = None
00129 bpki_glue = None
00130
00131 def serve_fetch_one_maybe(self):
00132 """
00133 Find the client object on which a get, set, or destroy method
00134 should operate, or which would conflict with a create method.
00135 """
00136 return self.sql_fetch_where1(self.gctx, "client_handle = %s", self.client_handle)
00137
00138 def serve_fetch_all(self):
00139 """Find client objects on which a list method should operate."""
00140 return self.sql_fetch_all(self.gctx)
00141
00142 def check_allowed_uri(self, uri):
00143 if not uri.startswith(self.base_uri):
00144 raise rpki.exceptions.ForbiddenURI
00145
00146 class publication_object_elt(rpki.xml_utils.base_elt, publication_namespace):
00147 """
00148 Virtual class for publishable objects. These have very similar
00149 syntax, differences lie in underlying datatype and methods. XML
00150 methods are a little different from the pattern used for objects
00151 that support the create/set/get/list/destroy actions, but
00152 publishable objects don't go in SQL either so these classes would be
00153 different in any case.
00154 """
00155
00156 attributes = ("action", "tag", "client_handle", "uri")
00157 payload_type = None
00158 payload = None
00159
00160 def endElement(self, stack, name, text):
00161 """
00162 Handle a publishable element element.
00163 """
00164 assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
00165 if text:
00166 self.payload = self.payload_type(Base64 = text)
00167 stack.pop()
00168
00169 def toXML(self):
00170 """
00171 Generate XML element for publishable object.
00172 """
00173 elt = self.make_elt()
00174 if self.payload:
00175 elt.text = self.payload.get_Base64()
00176 return elt
00177
00178 def serve_dispatch(self, r_msg, cb, eb):
00179 """
00180 Action dispatch handler.
00181 """
00182 try:
00183 if self.client is None:
00184 raise rpki.exceptions.BadQuery, "Client query received on control channel"
00185 dispatch = { "publish" : self.serve_publish,
00186 "withdraw" : self.serve_withdraw }
00187 if self.action not in dispatch:
00188 raise rpki.exceptions.BadQuery, "Unexpected query: action %s" % self.action
00189 self.client.check_allowed_uri(self.uri)
00190 dispatch[self.action]()
00191 r_pdu = self.__class__()
00192 r_pdu.action = self.action
00193 r_pdu.tag = self.tag
00194 r_pdu.uri = self.uri
00195 r_msg.append(r_pdu)
00196 cb()
00197 except rpki.exceptions.NoObjectAtURI, e:
00198
00199
00200 r_msg.append(report_error_elt.from_exception(e, self.tag))
00201 cb()
00202
00203 def serve_publish(self):
00204 """
00205 Publish an object.
00206 """
00207 rpki.log.info("Publishing %r as %r" % (self.payload, self.uri))
00208 filename = self.uri_to_filename()
00209 filename_tmp = filename + ".tmp"
00210 dirname = os.path.dirname(filename)
00211 if not os.path.isdir(dirname):
00212 os.makedirs(dirname)
00213 f = open(filename_tmp, "wb")
00214 f.write(self.payload.get_DER())
00215 f.close()
00216 os.rename(filename_tmp, filename)
00217
00218 def serve_withdraw(self):
00219 """
00220 Withdraw an object.
00221 """
00222 rpki.log.info("Withdrawing %r" % (self.uri,))
00223 filename = self.uri_to_filename()
00224 try:
00225 os.remove(filename)
00226 except OSError, e:
00227 if e.errno == errno.ENOENT:
00228 raise rpki.exceptions.NoObjectAtURI, "No object published at %r" % self.uri
00229 else:
00230 raise
00231
00232 def uri_to_filename(self):
00233 """
00234 Convert a URI to a local filename.
00235 """
00236 if not self.uri.startswith("rsync://"):
00237 raise rpki.exceptions.BadURISyntax, self.uri
00238 path = self.uri.split("/")[3:]
00239 if not self.gctx.publication_multimodule:
00240 del path[0]
00241 path.insert(0, self.gctx.publication_base.rstrip("/"))
00242 filename = "/".join(path)
00243 if "/../" in filename or filename.endswith("/.."):
00244 raise rpki.exceptions.BadURISyntax, filename
00245 return filename
00246
00247 @classmethod
00248 def make_publish(cls, uri, obj, tag = None):
00249 """
00250 Construct a publication PDU.
00251 """
00252 assert cls.payload_type is not None and type(obj) is cls.payload_type
00253 return cls.make_pdu(action = "publish", uri = uri, payload = obj, tag = tag)
00254
00255 @classmethod
00256 def make_withdraw(cls, uri, obj, tag = None):
00257 """
00258 Construct a withdrawal PDU.
00259 """
00260 assert cls.payload_type is not None and type(obj) is cls.payload_type
00261 return cls.make_pdu(action = "withdraw", uri = uri, tag = tag)
00262
00263 def raise_if_error(self):
00264 """
00265 No-op, since this is not a <report_error/> PDU.
00266 """
00267 pass
00268
00269 class certificate_elt(publication_object_elt):
00270 """
00271 <certificate/> element.
00272 """
00273
00274 element_name = "certificate"
00275 payload_type = rpki.x509.X509
00276
00277 class crl_elt(publication_object_elt):
00278 """
00279 <crl/> element.
00280 """
00281
00282 element_name = "crl"
00283 payload_type = rpki.x509.CRL
00284
00285 class manifest_elt(publication_object_elt):
00286 """
00287 <manifest/> element.
00288 """
00289
00290 element_name = "manifest"
00291 payload_type = rpki.x509.SignedManifest
00292
00293 class roa_elt(publication_object_elt):
00294 """
00295 <roa/> element.
00296 """
00297
00298 element_name = "roa"
00299 payload_type = rpki.x509.ROA
00300
00301 publication_object_elt.obj2elt = dict((e.payload_type, e) for e in (certificate_elt, crl_elt, manifest_elt, roa_elt))
00302
00303 class report_error_elt(rpki.xml_utils.text_elt, publication_namespace):
00304 """
00305 <report_error/> element.
00306 """
00307
00308 element_name = "report_error"
00309 attributes = ("tag", "error_code")
00310 text_attribute = "error_text"
00311
00312 error_text = None
00313
00314 @classmethod
00315 def from_exception(cls, e, tag = None):
00316 """
00317 Generate a <report_error/> element from an exception.
00318 """
00319 self = cls()
00320 self.tag = tag
00321 self.error_code = e.__class__.__name__
00322 self.error_text = str(e)
00323 return self
00324
00325 def __str__(self):
00326 s = ""
00327 if getattr(self, "tag", None) is not None:
00328 s += "[%s] " % self.tag
00329 s += self.error_code
00330 if getattr(self, "error_text", None) is not None:
00331 s += ": " + self.error_text
00332 return s
00333
00334 def raise_if_error(self):
00335 """
00336 Raise exception associated with this <report_error/> PDU.
00337 """
00338 t = rpki.exceptions.__dict__.get(self.error_code)
00339 if isinstance(t, type) and issubclass(t, rpki.exceptions.RPKI_Exception):
00340 raise t, getattr(self, "text", None)
00341 else:
00342 raise rpki.exceptions.BadPublicationReply, "Unexpected response from pubd: %s" % self
00343
00344 class msg(rpki.xml_utils.msg, publication_namespace):
00345 """
00346 Publication PDU.
00347 """
00348
00349
00350
00351 version = 1
00352
00353
00354
00355 pdus = dict((x.element_name, x)
00356 for x in (config_elt, client_elt, certificate_elt, crl_elt, manifest_elt, roa_elt, report_error_elt))
00357
00358 def serve_top_level(self, gctx, client, cb):
00359 """
00360 Serve one msg PDU.
00361 """
00362 if not self.is_query():
00363 raise rpki.exceptions.BadQuery, "Message type is not query"
00364 r_msg = self.__class__.reply()
00365
00366 def loop(iterator, q_pdu):
00367
00368 def fail(e):
00369 if not isinstance(e, rpki.exceptions.NotFound):
00370 rpki.log.traceback()
00371 r_msg.append(report_error_elt.from_exception(e, q_pdu.tag))
00372 cb(r_msg)
00373
00374 try:
00375 q_pdu.gctx = gctx
00376 q_pdu.client = client
00377 q_pdu.serve_dispatch(r_msg, iterator, fail)
00378 except (rpki.async.ExitNow, SystemExit):
00379 raise
00380 except Exception, e:
00381 fail(e)
00382
00383 def done():
00384 cb(r_msg)
00385
00386 rpki.async.iterator(self, loop, done)
00387
00388 class sax_handler(rpki.xml_utils.sax_handler):
00389 """
00390 SAX handler for publication protocol.
00391 """
00392
00393 pdu = msg
00394 name = "msg"
00395 version = "1"
00396
00397 class cms_msg(rpki.x509.XML_CMS_object):
00398 """
00399 Class to hold a CMS-signed publication PDU.
00400 """
00401
00402 encoding = "us-ascii"
00403 schema = rpki.relaxng.publication
00404 saxify = sax_handler.saxify