1
2
3
4
5
6
7
8
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
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
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
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
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
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
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
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
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
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
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
282 if addr.username != None:
283 val = base64.encodestring(addr.user)
284 r.putheader('Authorization', 'Basic ' + val.replace('\012', ''))
285
286
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
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
319 return data.decode('utf-8')
320
321
323 """ Represents an error of a HTTP request.
324 """
325
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
339 return "<HTTPError %s %s>" % (self.code, self.msg)
340
342 return (self.code, self.msg, )
343