Package brisa :: Package upnp :: Module soap
[hide private]
[frames] | no frames]

Source Code for Module brisa.upnp.soap

  1  # Licensed under the MIT license 
  2  # http://opensource.org/licenses/mit-license.php or see LICENSE file. 
  3  # Copyright 2007-2008 Brisa Team <brisa-develop@garage.maemo.org> 
  4  # 
  5  # Copyright 2001 - Cayce Ullman <http://pywebsvcs.sourceforge.net> 
  6  # Copyright 2001 - Brian Matthews <http://pywebsvcs.sourceforge.net> 
  7  # Copyright 2001-2003 - Pfizer <http://pywebsvcs.sourceforge.net> 
  8  # Copyright 2007-2008 - Frank Scholz <coherence@beebits.net> 
  9   
 10  """ Parses and builds SOAP calls transparently. 
 11  """ 
 12   
 13  import httplib 
 14  import exceptions 
 15   
 16  from xml.etree import ElementTree 
 17   
 18  from brisa.core.network import parse_xml, parse_url 
 19   
 20   
 21  # SOAP constants 
 22   
 23   
 24  NS_SOAP_ENV = "{http://schemas.xmlsoap.org/soap/envelope/}" 
 25  NS_SOAP_ENC = "{http://schemas.xmlsoap.org/soap/encoding/}" 
 26  NS_XSI = "{http://www.w3.org/1999/XMLSchema-instance}" 
 27  NS_XSD = "{http://www.w3.org/1999/XMLSchema}" 
 28   
 29  SOAP_ENCODING = "http://schemas.xmlsoap.org/soap/encoding/" 
 30   
 31  UPNPERRORS = {401: 'Invalid Action', 
 32                402: 'Invalid Args', 
 33                501: 'Action Failed', 
 34                600: 'Argument Value Invalid', 
 35                601: 'Argument Value Out of Range', 
 36                602: 'Optional Action Not Implemented', 
 37                603: 'Out Of Memory', 
 38                604: 'Human Intervention Required', 
 39                605: 'String Argument Too Long', 
 40                606: 'Action Not Authorized', 
 41                607: 'Signature Failure', 
 42                608: 'Signature Missing', 
 43                609: 'Not Encrypted', 
 44                610: 'Invalid Sequence', 
 45                611: 'Invalid Control URL', 
 46                612: 'No Such Session', } 
 47   
 48   
49 -def build_soap_error(status, description='without words'):
50 """ Builds an UPnP SOAP error message. 51 52 @param status: error code 53 @param description: error default description 54 55 @type status: integer 56 @type description: string 57 58 @return: soap call representing the error 59 @rtype: string 60 """ 61 root = ElementTree.Element('s:Fault') 62 ElementTree.SubElement(root, 'faultcode').text = 's:Client' 63 ElementTree.SubElement(root, 'faultstring').text = 'UPnPError' 64 e = ElementTree.SubElement(root, 'detail') 65 e = ElementTree.SubElement(e, 'UPnPError') 66 e.attrib['xmlns'] = 'urn:schemas-upnp-org:control-1-0' 67 ElementTree.SubElement(e, 'errorCode').text = str(status) 68 ElementTree.SubElement(e, 'errorDescription').text = UPNPERRORS.get(status, 69 description) 70 71 return build_soap_call(None, root, encoding=None)
72 73
74 -def build_soap_call(method, arguments, encoding=SOAP_ENCODING, 75 envelope_attrib=None, typed=None):
76 """ Builds a soap call. 77 78 @param method: method for the soap call. If set to None, the method element 79 will be omitted and arguments will be added directly to the body (error 80 message) 81 @param arguments: arguments for the call 82 @param encoding: encoding for the call 83 @param envelope_attrib: envelope attribute 84 @param typed: True if typed 85 86 @type method: string or None 87 @type arguments: dict or ElementTree.Element 88 @type encoding: string 89 @type envelope_attrib: list 90 @type typed: boolean or None 91 92 @return: soap call 93 @rtype: string 94 """ 95 envelope = ElementTree.Element("s:Envelope") 96 if envelope_attrib: 97 for n in envelope_attrib: 98 envelope.attrib.update({n[0]: n[1]}) 99 else: 100 envelope.attrib.update({'s:encodingStyle': 101 "http://schemas.xmlsoap.org/soap/encoding/"}) 102 envelope.attrib.update({'xmlns:s': 103 "http://schemas.xmlsoap.org/soap/envelope/"}) 104 105 body = ElementTree.SubElement(envelope, "s:Body") 106 107 if method: 108 re = ElementTree.SubElement(body, method) 109 if encoding: 110 re.set("%sencodingStyle" % NS_SOAP_ENV, encoding) 111 else: 112 re = body 113 114 # append the arguments 115 if isinstance(arguments, dict): 116 type_map = {str: 'xsd:string', 117 unicode: 'xsd:string', 118 int: 'xsd:int', 119 long: 'xsd:int', 120 float: 'xsd:float', 121 bool: 'xsd:boolean'} 122 123 for arg_name, arg_val in arguments.iteritems(): 124 arg_type = type_map[type(arg_val)] 125 if arg_type == 'xsd:string' and type(arg_val) == unicode: 126 arg_val = arg_val.encode('utf-8') 127 if arg_type == 'xsd:int' or arg_type == 'xsd:float': 128 arg_val = str(arg_val) 129 if arg_type == 'xsd:boolean': 130 arg_val = '1' if arg_val else '0' 131 132 e = ElementTree.SubElement(re, arg_name) 133 if typed and arg_type: 134 if not isinstance(type, ElementTree.QName): 135 arg_type = ElementTree.QName( 136 "http://www.w3.org/1999/XMLSchema", arg_type) 137 e.set('%stype' % NS_XSI, arg_type) 138 e.text = arg_val 139 else: 140 re.append(arguments) 141 142 preamble = """<?xml version="1.0" encoding="utf-8"?>""" 143 return '%s%s' % (preamble, ElementTree.tostring(envelope, 'utf-8'))
144 145
146 -def __decode_result(element):
147 """ Decodes the result out of an Element. Returns the text, if possible. 148 149 @param element: element to decode the result 150 @type element Element 151 152 @return: text of the result 153 @rtype: string 154 """ 155 type = element.get('{http://www.w3.org/1999/XMLSchema-instance}type') 156 if type is not None: 157 try: 158 prefix, local = type.split(":") 159 if prefix == 'xsd': 160 type = local 161 except ValueError: 162 pass 163 164 if type == "integer" or type == "int": 165 return int(element.text) 166 if type == "float" or type == "double": 167 return float(element.text) 168 if type == "boolean": 169 return element.text == "true" 170 171 return element.text or ""
172 173
174 -def parse_soap_call(data):
175 """ Parses a soap call and returns a 4-tuple. 176 177 @param data: raw soap XML call data 178 @type data: string 179 180 @return: 4-tuple (method_name, args, kwargs, namespace) 181 @rtype: tuple 182 """ 183 tree = parse_xml(data) 184 body = tree.find('{http://schemas.xmlsoap.org/soap/envelope/}Body') 185 method = body.getchildren()[0] 186 method_name = method.tag 187 ns = None 188 189 if method_name.startswith('{') and method_name.rfind('}') > 1: 190 ns, method_name = method_name[1:].split('}') 191 192 args = [] 193 kwargs = {} 194 for child in method.getchildren(): 195 kwargs[child.tag] = __decode_result(child) 196 args.append(kwargs[child.tag]) 197 198 return method_name, args, kwargs, ns
199 200
201 -class SOAPProxy(object):
202 """ Proxy for making remote SOAP calls Based on twisted.web.soap.Proxy 203 and SOAPpy. 204 """ 205
206 - def __init__(self, url, namespace=None):
207 """ Constructor for the SOAPProxy class. 208 209 @param url: remote SOAP server 210 @param namespace: calls namespace 211 212 @type url: string 213 @type namespace: tuple 214 """ 215 self.url = url 216 self.namespace = namespace
217
218 - def call_remote(self, soapmethod, **kwargs):
219 """ Performs a remote SOAP call. 220 221 @param soapmethod: method to be called 222 @param kwargs: args to be passed, can be named. 223 224 @type soapmethod: string 225 @type kwargs: dictionary 226 227 @return: the result text of the soap call. 228 @rtype: string 229 """ 230 ns = self.namespace 231 soapaction = '%s#%s' % (ns[1], soapmethod) 232 payload = build_soap_call('{%s}%s' % (ns[1], soapmethod), 233 kwargs, encoding=None) 234 result = HTTPTransport().call(self.url, payload, ns, 235 soapaction=soapaction, encoding='utf-8') 236 _, _, res, _ = parse_soap_call(result) 237 return res
238 239
240 -class HTTPTransport(object):
241 """ Wrapper class for a HTTP SOAP call. It contain the call() method that 242 can perform calls and return the response payload. 243 """ 244
245 - def call(self, addr, data, namespace, soapaction=None, encoding=None):
246 """ Builds and performs an HTTP request. Returns the response payload. 247 248 @param addr: address to receive the request in the form 249 schema://hostname:port 250 @param data: data to be sent 251 @param soapaction: soap action to be called 252 @param encoding: encoding for the message 253 254 @type addr: string 255 @type data: string 256 @type soapaction: string 257 @type encoding: string 258 259 @return: response payload 260 @rtype: string 261 """ 262 # Build a request 263 addr = parse_url(addr) 264 real_addr = '%s:%d' % (addr.hostname, addr.port) 265 real_path = addr.path 266 267 if addr.scheme == 'https': 268 r = httplib.HTTPS(real_addr) 269 else: 270 r = httplib.HTTP(real_addr) 271 272 r.putrequest("POST", real_path) 273 r.putheader("Host", addr.hostname) 274 r.putheader("User-agent", 'BRISA SERVER') 275 t = 'text/xml' 276 if encoding: 277 t += '; charset="%s"' % encoding 278 r.putheader("Content-type", t) 279 r.putheader("Content-length", str(len(data))) 280 281 # if user is not a user:passwd format 282 if addr.username != None: 283 val = base64.encodestring(addr.user) 284 r.putheader('Authorization', 'Basic ' + val.replace('\012', '')) 285 286 # This fixes sending either "" or "None" 287 if soapaction: 288 r.putheader("SOAPAction", '"%s"' % soapaction) 289 else: 290 r.putheader("SOAPAction", "") 291 292 r.endheaders() 293 r.send(data) 294 295 #read response line 296 code, msg, headers = r.getreply() 297 298 content_type = headers.get("content-type", "text/xml") 299 content_length = headers.get("Content-length") 300 if content_length == None: 301 data = r.getfile().read() 302 message_len = len(data) 303 else: 304 message_len = int(content_length) 305 data = r.getfile().read(message_len) 306 307 def startswith(string, val): 308 return string[0:len(val)] == val
309 310 311 if code == 500 and not \ 312 (startswith(content_type, "text/xml") and message_len > 0): 313 raise HTTPError(code, msg) 314 315 if code not in (200, 500): 316 raise HTTPError(code, msg) 317 318 #return response payload 319 return data.decode('utf-8')
320 321
322 -class HTTPError(exceptions.Exception):
323 """ Represents an error of a HTTP request. 324 """ 325
326 - def __init__(self, code, msg):
327 """ Constructor for the HTTPError class. 328 329 @param code: error code 330 @param msg: error message 331 332 @type code: string 333 @type msg: string 334 """ 335 self.code = code 336 self.msg = msg
337
338 - def __repr__(self):
339 return "<HTTPError %s %s>" % (self.code, self.msg)
340
341 - def __call___(self):
342 return (self.code, self.msg, )
343