00001 """XML utilities.
00002
00003 $Id: xml_utils.py 1873 2008-06-12 02:49:41Z sra $
00004
00005 Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00006
00007 Permission to use, copy, modify, and distribute this software for any
00008 purpose with or without fee is hereby granted, provided that the above
00009 copyright notice and this permission notice appear in all copies.
00010
00011 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00012 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00013 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00014 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00015 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00016 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00017 PERFORMANCE OF THIS SOFTWARE.
00018 """
00019
00020 import xml.sax, lxml.sax, lxml.etree, base64
00021
00022 class sax_handler(xml.sax.handler.ContentHandler):
00023 """SAX handler for RPKI protocols.
00024
00025 This class provides some basic amenities for parsing protocol XML of
00026 the kind we use in the RPKI protocols, including whacking all the
00027 protocol element text into US-ASCII, simplifying accumulation of
00028 text fields, and hiding some of the fun relating to XML namespaces.
00029
00030 General assumption: by the time this parsing code gets invoked, the
00031 XML has already passed RelaxNG validation, so we only have to check
00032 for errors that the schema can't catch, and we don't have to play as
00033 many XML namespace games.
00034 """
00035
00036 def __init__(self):
00037 """Initialize SAX handler."""
00038 self.text = ""
00039 self.stack = []
00040
00041 def startElementNS(self, name, qname, attrs):
00042 """Redirect startElementNS() events to startElement()."""
00043 return self.startElement(name[1], attrs)
00044
00045 def endElementNS(self, name, qname):
00046 """Redirect endElementNS() events to endElement()."""
00047 return self.endElement(name[1])
00048
00049 def characters(self, content):
00050 """Accumulate a chuck of element content (text)."""
00051 self.text += content
00052
00053 def startElement(self, name, attrs):
00054 """Handle startElement() events.
00055
00056 We maintain a stack of nested elements under construction so that
00057 we can feed events directly to the current element rather than
00058 having to pass them through all the nesting elements.
00059
00060 If the stack is empty, this event is for the outermost element, so
00061 we call a virtual method to create the corresponding object and
00062 that's the object we'll be returning as our final result.
00063 """
00064 a = dict()
00065 for k,v in attrs.items():
00066 if isinstance(k, tuple):
00067 if k == ("http://www.w3.org/XML/1998/namespace", "lang"):
00068 k = "xml:lang"
00069 else:
00070 assert k[0] is None
00071 k = k[1]
00072 a[k.encode("ascii")] = v.encode("ascii")
00073 if len(self.stack) == 0:
00074 assert not hasattr(self, "result")
00075 self.result = self.create_top_level(name, a)
00076 self.stack.append(self.result)
00077 self.stack[-1].startElement(self.stack, name, a)
00078
00079 def endElement(self, name):
00080 """Handle endElement() events.
00081
00082 Mostly this means handling any accumulated element text.
00083 """
00084 text = self.text.encode("ascii").strip()
00085 self.text = ""
00086 self.stack[-1].endElement(self.stack, name, text)
00087
00088 @classmethod
00089 def saxify(cls, elt):
00090 """Create a one-off SAX parser, parse an ETree, return the result.
00091 """
00092 self = cls()
00093 lxml.sax.saxify(elt, self)
00094 return self.result
00095
00096 def create_top_level(self, name, attrs):
00097 """Handle top-level PDU for this protocol."""
00098 assert name == self.name and attrs["version"] == self.version
00099 return self.pdu()
00100
00101 class base_elt(object):
00102 """Virtual base class for XML message elements. The left-right and
00103 publication protocols use this. At least for now, the up-down
00104 protocol does not, due to different design assumptions.
00105 """
00106
00107
00108
00109 attributes = ()
00110
00111
00112
00113 elements = ()
00114
00115
00116
00117 booleans = ()
00118
00119 def startElement(self, stack, name, attrs):
00120 """Default startElement() handler: just process attributes."""
00121 if name not in self.elements:
00122 assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
00123 self.read_attrs(attrs)
00124
00125 def endElement(self, stack, name, text):
00126 """Default endElement() handler: just pop the stack."""
00127 assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
00128 stack.pop()
00129
00130 def toXML(self):
00131 """Default toXML() element generator."""
00132 return self.make_elt()
00133
00134 def read_attrs(self, attrs):
00135 """Template-driven attribute reader."""
00136 for key in self.attributes:
00137 val = attrs.get(key, None)
00138 if isinstance(val, str) and val.isdigit():
00139 val = long(val)
00140 setattr(self, key, val)
00141 for key in self.booleans:
00142 setattr(self, key, attrs.get(key, False))
00143
00144 def make_elt(self):
00145 """XML element constructor."""
00146 elt = lxml.etree.Element("{%s}%s" % (self.xmlns, self.element_name), nsmap = self.nsmap)
00147 for key in self.attributes:
00148 val = getattr(self, key, None)
00149 if val is not None:
00150 elt.set(key, str(val))
00151 for key in self.booleans:
00152 if getattr(self, key, False):
00153 elt.set(key, "yes")
00154 return elt
00155
00156 def make_b64elt(self, elt, name, value = None):
00157 """Constructor for Base64-encoded subelement."""
00158 if value is None:
00159 value = getattr(self, name, None)
00160 if value is not None:
00161 lxml.etree.SubElement(elt, "{%s}%s" % (self.xmlns, name), nsmap = self.nsmap).text = base64.b64encode(value)
00162
00163 def __str__(self):
00164 """Convert a base_elt object to string format."""
00165 lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii")
00166
00167 @classmethod
00168 def make_pdu(cls, **kargs):
00169 """Generic PDU constructor."""
00170 self = cls()
00171 for k,v in kargs.items():
00172 if isinstance(v, bool):
00173 v = 1 if v else 0
00174 setattr(self, k, v)
00175 return self
00176
00177 class data_elt(base_elt):
00178 """Virtual base class for PDUs that map to SQL objects. These
00179 objects all implement the create/set/get/list/destroy action
00180 attribute.
00181 """
00182
00183 def endElement(self, stack, name, text):
00184 """Default endElement handler for SQL-based objects. This assumes
00185 that sub-elements are Base64-encoded using the sql_template mechanism.
00186 """
00187 if name in self.elements:
00188 elt_type = self.sql_template.map.get(name)
00189 assert elt_type is not None, "Couldn't find element type for %s, stack %s" % (name, stack)
00190 setattr(self, name, elt_type(Base64 = text))
00191 else:
00192 assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
00193 stack.pop()
00194
00195 def toXML(self):
00196 """Default element generator for SQL-based objects. This assumes
00197 that sub-elements are Base64-encoded DER objects.
00198 """
00199 elt = self.make_elt()
00200 for i in self.elements:
00201 x = getattr(self, i, None)
00202 if x and not x.empty():
00203 self.make_b64elt(elt, i, x.get_DER())
00204 return elt
00205
00206 def make_reply(self, r_pdu = None):
00207 """Construct a reply PDU."""
00208 if r_pdu is None:
00209 r_pdu = self.__class__()
00210 self.make_reply_clone_hook(r_pdu)
00211 setattr(r_pdu, self.sql_template.index, getattr(self, self.sql_template.index))
00212 else:
00213 for b in r_pdu.booleans:
00214 setattr(r_pdu, b, False)
00215 r_pdu.action = self.action
00216 r_pdu.tag = self.tag
00217 return r_pdu
00218
00219 def make_reply_clone_hook(self, r_pdu):
00220 """Overridable hook."""
00221 pass
00222
00223 def serve_pre_save_hook(self, q_pdu, r_pdu):
00224 """Overridable hook."""
00225 pass
00226
00227 def serve_post_save_hook(self, q_pdu, r_pdu):
00228 """Overridable hook."""
00229 pass
00230
00231 def serve_create(self, r_msg):
00232 """Handle a create action."""
00233 r_pdu = self.make_reply()
00234 self.serve_pre_save_hook(self, r_pdu)
00235 self.sql_store()
00236 setattr(r_pdu, self.sql_template.index, getattr(self, self.sql_template.index))
00237 self.serve_post_save_hook(self, r_pdu)
00238 r_msg.append(r_pdu)
00239
00240 def serve_set(self, r_msg):
00241 """Handle a set action."""
00242 db_pdu = self.serve_fetch_one()
00243 r_pdu = self.make_reply()
00244 for a in db_pdu.sql_template.columns[1:]:
00245 v = getattr(self, a)
00246 if v is not None:
00247 setattr(db_pdu, a, v)
00248 db_pdu.sql_mark_dirty()
00249 db_pdu.serve_pre_save_hook(self, r_pdu)
00250 db_pdu.sql_store()
00251 db_pdu.serve_post_save_hook(self, r_pdu)
00252 r_msg.append(r_pdu)
00253
00254 def serve_get(self, r_msg):
00255 """Handle a get action."""
00256 r_pdu = self.serve_fetch_one()
00257 self.make_reply(r_pdu)
00258 r_msg.append(r_pdu)
00259
00260 def serve_list(self, r_msg):
00261 """Handle a list action for non-self objects."""
00262 for r_pdu in self.serve_fetch_all():
00263 self.make_reply(r_pdu)
00264 r_msg.append(r_pdu)
00265
00266 def serve_destroy(self, r_msg):
00267 """Handle a destroy action."""
00268 db_pdu = self.serve_fetch_one()
00269 db_pdu.sql_delete()
00270 r_msg.append(self.make_reply())
00271
00272 def serve_dispatch(self, r_msg):
00273 """Action dispatch handler."""
00274 dispatch = { "create" : self.serve_create,
00275 "set" : self.serve_set,
00276 "get" : self.serve_get,
00277 "list" : self.serve_list,
00278 "destroy" : self.serve_destroy }
00279 if self.action not in dispatch:
00280 raise rpki.exceptions.BadQuery, "Unexpected query: action %s" % self.action
00281 dispatch[self.action](r_msg)
00282
00283 def unimplemented_control(self, *controls):
00284 """Uniform handling for unimplemented control operations."""
00285 unimplemented = [x for x in controls if getattr(self, x, False)]
00286 if unimplemented:
00287 raise rpki.exceptions.NotImplementedYet, "Unimplemented control %s" % ", ".join(unimplemented)
00288
00289 class msg(list):
00290 """Generic top-level PDU."""
00291
00292 def startElement(self, stack, name, attrs):
00293 """Handle top-level PDU."""
00294 if name == "msg":
00295 assert self.version == int(attrs["version"])
00296 self.type = attrs["type"]
00297 else:
00298 elt = self.pdus[name]()
00299 self.append(elt)
00300 stack.append(elt)
00301 elt.startElement(stack, name, attrs)
00302
00303 def endElement(self, stack, name, text):
00304 """Handle top-level PDU."""
00305 assert name == "msg", "Unexpected name %s, stack %s" % (name, stack)
00306 assert len(stack) == 1
00307 stack.pop()
00308
00309 def __str__(self):
00310 """Convert msg object to string."""
00311 lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii")
00312
00313 def toXML(self):
00314 """Generate top-level PDU."""
00315 elt = lxml.etree.Element("{%s}msg" % (self.xmlns), nsmap = self.nsmap, version = str(self.version), type = self.type)
00316 elt.extend([i.toXML() for i in self])
00317 return elt