Add timestamp offset for block header
[p2pool.git] / SOAPpy / Client.py
1 from __future__ import nested_scopes
2
3 """
4 ################################################################################
5 #
6 # SOAPpy - Cayce Ullman       (cayce@actzero.com)
7 #          Brian Matthews     (blm@actzero.com)
8 #          Gregory Warnes     (Gregory.R.Warnes@Pfizer.com)
9 #          Christopher Blunck (blunck@gst.com)
10 #
11 ################################################################################
12 # Copyright (c) 2003, Pfizer
13 # Copyright (c) 2001, Cayce Ullman.
14 # Copyright (c) 2001, Brian Matthews.
15 #
16 # All rights reserved.
17 #
18 # Redistribution and use in source and binary forms, with or without
19 # modification, are permitted provided that the following conditions are met:
20 # Redistributions of source code must retain the above copyright notice, this
21 # list of conditions and the following disclaimer.
22 #
23 # Redistributions in binary form must reproduce the above copyright notice,
24 # this list of conditions and the following disclaimer in the documentation
25 # and/or other materials provided with the distribution.
26 #
27 # Neither the name of actzero, inc. nor the names of its contributors may
28 # be used to endorse or promote products derived from this software without
29 # specific prior written permission.
30 #
31 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
32 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
33 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34 # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
35 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
36 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
38 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
39 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
40 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41 #
42 ################################################################################
43 """
44 ident = '$Id: Client.py 1496 2010-03-04 23:46:17Z pooryorick $'
45
46 from version import __version__
47
48 #import xml.sax
49 import urllib
50 from types import *
51 import re
52 import base64
53 import socket, httplib
54 from httplib import HTTPConnection, HTTP
55 import Cookie
56
57 # SOAPpy modules
58 from Errors      import *
59 from Config      import Config
60 from Parser      import parseSOAPRPC
61 from SOAPBuilder import buildSOAP
62 from Utilities   import *
63 from Types       import faultType, simplify
64
65 ################################################################################
66 # Client
67 ################################################################################
68
69
70 def SOAPUserAgent():
71     return "SOAPpy " + __version__ + " (pywebsvcs.sf.net)"
72
73
74 class SOAPAddress:
75     def __init__(self, url, config = Config):
76         proto, uri = urllib.splittype(url)
77
78         # apply some defaults
79         if uri[0:2] != '//':
80             if proto != None:
81                 uri = proto + ':' + uri
82
83             uri = '//' + uri
84             proto = 'http'
85
86         host, path = urllib.splithost(uri)
87
88         try:
89             int(host)
90             host = 'localhost:' + host
91         except:
92             pass
93
94         if not path:
95             path = '/'
96
97         if proto not in ('http', 'https', 'httpg'):
98             raise IOError, "unsupported SOAP protocol"
99         if proto == 'httpg' and not config.GSIclient:
100             raise AttributeError, \
101                   "GSI client not supported by this Python installation"
102         if proto == 'https' and not config.SSLclient:
103             raise AttributeError, \
104                 "SSL client not supported by this Python installation"
105
106         self.user,host = urllib.splituser(host)
107         self.proto = proto
108         self.host = host
109         self.path = path
110
111     def __str__(self):
112         return "%(proto)s://%(host)s%(path)s" % self.__dict__
113
114     __repr__ = __str__
115
116 class SOAPTimeoutError(socket.timeout):
117     '''This exception is raised when a timeout occurs in SOAP operations'''
118     pass
119
120 class HTTPConnectionWithTimeout(HTTPConnection):
121     '''Extend HTTPConnection for timeout support'''
122
123     def __init__(self, host, port=None, strict=None, timeout=None):
124         HTTPConnection.__init__(self, host, port, strict)
125         self._timeout = timeout
126
127     def connect(self):
128         HTTPConnection.connect(self)
129         if self.sock and self._timeout:
130             self.sock.settimeout(self._timeout) 
131
132
133 class HTTPWithTimeout(HTTP):
134
135     _connection_class = HTTPConnectionWithTimeout
136
137     ## this __init__ copied from httplib.HTML class
138     def __init__(self, host='', port=None, strict=None, timeout=None):
139         "Provide a default host, since the superclass requires one."
140
141         # some joker passed 0 explicitly, meaning default port
142         if port == 0:
143             port = None
144
145         # Note that we may pass an empty string as the host; this will throw
146         # an error when we attempt to connect. Presumably, the client code
147         # will call connect before then, with a proper host.
148         self._setup(self._connection_class(host, port, strict, timeout))
149
150 class HTTPTransport:
151             
152
153     def __init__(self):
154         self.cookies = Cookie.SimpleCookie();
155
156     def getNS(self, original_namespace, data):
157         """Extract the (possibly extended) namespace from the returned
158         SOAP message."""
159
160         if type(original_namespace) == StringType:
161             pattern="xmlns:\w+=['\"](" + original_namespace + "[^'\"]*)['\"]"
162             match = re.search(pattern, data)
163             if match:
164                 return match.group(1)
165             else:
166                 return original_namespace
167         else:
168             return original_namespace
169     
170     def __addcookies(self, r):
171         '''Add cookies from self.cookies to request r
172         '''
173         for cname, morsel in self.cookies.items():
174             attrs = []
175             value = morsel.get('version', '')
176             if value != '' and value != '0':
177                 attrs.append('$Version=%s' % value)
178             attrs.append('%s=%s' % (cname, morsel.coded_value))
179             value = morsel.get('path')
180             if value:
181                 attrs.append('$Path=%s' % value)
182             value = morsel.get('domain')
183             if value:
184                 attrs.append('$Domain=%s' % value)
185             r.putheader('Cookie', "; ".join(attrs))
186     
187     def call(self, addr, data, namespace, soapaction = None, encoding = None,
188         http_proxy = None, config = Config, timeout=None):
189
190         if not isinstance(addr, SOAPAddress):
191             addr = SOAPAddress(addr, config)
192
193         # Build a request
194         if http_proxy:
195             real_addr = http_proxy
196             real_path = addr.proto + "://" + addr.host + addr.path
197         else:
198             real_addr = addr.host
199             real_path = addr.path
200
201         if addr.proto == 'httpg':
202             from pyGlobus.io import GSIHTTP
203             r = GSIHTTP(real_addr, tcpAttr = config.tcpAttr)
204         elif addr.proto == 'https':
205             r = httplib.HTTPS(real_addr, key_file=config.SSL.key_file, cert_file=config.SSL.cert_file)
206         else:
207             r = HTTPWithTimeout(real_addr, timeout=timeout)
208
209         r.putrequest("POST", real_path)
210
211         r.putheader("Host", addr.host)
212         r.putheader("User-agent", SOAPUserAgent())
213         t = 'text/xml';
214         if encoding != None:
215             t += '; charset=%s' % encoding
216         r.putheader("Content-type", t)
217         r.putheader("Content-length", str(len(data)))
218         self.__addcookies(r);
219         
220         # if user is not a user:passwd format
221         #    we'll receive a failure from the server. . .I guess (??)
222         if addr.user != None:
223             val = base64.encodestring(addr.user) 
224             r.putheader('Authorization','Basic ' + val.replace('\012',''))
225
226         # This fixes sending either "" or "None"
227         if soapaction == None or len(soapaction) == 0:
228             r.putheader("SOAPAction", "")
229         else:
230             r.putheader("SOAPAction", '"%s"' % soapaction)
231
232         if config.dumpHeadersOut:
233             s = 'Outgoing HTTP headers'
234             debugHeader(s)
235             print "POST %s %s" % (real_path, r._http_vsn_str)
236             print "Host:", addr.host
237             print "User-agent: SOAPpy " + __version__ + " (http://pywebsvcs.sf.net)"
238             print "Content-type:", t
239             print "Content-length:", len(data)
240             print 'SOAPAction: "%s"' % soapaction
241             debugFooter(s)
242
243         r.endheaders()
244
245         if config.dumpSOAPOut:
246             s = 'Outgoing SOAP'
247             debugHeader(s)
248             print data,
249             if data[-1] != '\n':
250                 print
251             debugFooter(s)
252
253         # send the payload
254         r.send(data)
255
256         # read response line
257         code, msg, headers = r.getreply()
258
259         self.cookies = Cookie.SimpleCookie();
260         if headers:
261             content_type = headers.get("content-type","text/xml")
262             content_length = headers.get("Content-length")
263
264             for cookie in headers.getallmatchingheaders("Set-Cookie"):
265                 self.cookies.load(cookie);
266
267         else:
268             content_type=None
269             content_length=None
270
271         # work around OC4J bug which does '<len>, <len>' for some reaason
272         if content_length:
273             comma=content_length.find(',')
274             if comma>0:
275                 content_length = content_length[:comma]
276
277         # attempt to extract integer message size
278         try:
279             message_len = int(content_length)
280         except:
281             message_len = -1
282             
283         if message_len < 0:
284             # Content-Length missing or invalid; just read the whole socket
285             # This won't work with HTTP/1.1 chunked encoding
286             data = r.getfile().read()
287             message_len = len(data)
288         else:
289             data = r.getfile().read(message_len)
290
291         if(config.debug):
292             print "code=",code
293             print "msg=", msg
294             print "headers=", headers
295             print "content-type=", content_type
296             print "data=", data
297                 
298         if config.dumpHeadersIn:
299             s = 'Incoming HTTP headers'
300             debugHeader(s)
301             if headers.headers:
302                 print "HTTP/1.? %d %s" % (code, msg)
303                 print "\n".join(map (lambda x: x.strip(), headers.headers))
304             else:
305                 print "HTTP/0.9 %d %s" % (code, msg)
306             debugFooter(s)
307
308         def startswith(string, val):
309             return string[0:len(val)] == val
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 config.dumpSOAPIn:
316             s = 'Incoming SOAP'
317             debugHeader(s)
318             print data,
319             if (len(data)>0) and (data[-1] != '\n'):
320                 print
321             debugFooter(s)
322
323         if code not in (200, 500):
324             raise HTTPError(code, msg)
325
326
327         # get the new namespace
328         if namespace is None:
329             new_ns = None
330         else:
331             new_ns = self.getNS(namespace, data)
332         
333         # return response payload
334         return data, new_ns
335
336 ################################################################################
337 # SOAP Proxy
338 ################################################################################
339 class SOAPProxy:
340     def __init__(self, proxy, namespace = None, soapaction = None,
341                  header = None, methodattrs = None, transport = HTTPTransport,
342                  encoding = 'UTF-8', throw_faults = 1, unwrap_results = None,
343                  http_proxy=None, config = Config, noroot = 0,
344                  simplify_objects=None, timeout=None):
345
346         # Test the encoding, raising an exception if it's not known
347         if encoding != None:
348             ''.encode(encoding)
349
350         # get default values for unwrap_results and simplify_objects
351         # from config
352         if unwrap_results is None:
353             self.unwrap_results=config.unwrap_results
354         else:
355             self.unwrap_results=unwrap_results
356
357         if simplify_objects is None:
358             self.simplify_objects=config.simplify_objects
359         else:
360             self.simplify_objects=simplify_objects
361
362         self.proxy          = SOAPAddress(proxy, config)
363         self.namespace      = namespace
364         self.soapaction     = soapaction
365         self.header         = header
366         self.methodattrs    = methodattrs
367         self.transport      = transport()
368         self.encoding       = encoding
369         self.throw_faults   = throw_faults
370         self.http_proxy     = http_proxy
371         self.config         = config
372         self.noroot         = noroot
373         self.timeout        = timeout
374
375         # GSI Additions
376         if hasattr(config, "channel_mode") and \
377                hasattr(config, "delegation_mode"):
378             self.channel_mode = config.channel_mode
379             self.delegation_mode = config.delegation_mode
380         #end GSI Additions
381         
382     def invoke(self, method, args):
383         return self.__call(method, args, {})
384         
385     def __call(self, name, args, kw, ns = None, sa = None, hd = None,
386         ma = None):
387
388         ns = ns or self.namespace
389         ma = ma or self.methodattrs
390
391         if sa: # Get soapaction
392             if type(sa) == TupleType:
393                 sa = sa[0]
394         else:
395             if self.soapaction:
396                 sa = self.soapaction
397             else:
398                 sa = name
399                 
400         if hd: # Get header
401             if type(hd) == TupleType:
402                 hd = hd[0]
403         else:
404             hd = self.header
405
406         hd = hd or self.header
407
408         if ma: # Get methodattrs
409             if type(ma) == TupleType: ma = ma[0]
410         else:
411             ma = self.methodattrs
412         ma = ma or self.methodattrs
413
414         m = buildSOAP(args = args, kw = kw, method = name, namespace = ns,
415             header = hd, methodattrs = ma, encoding = self.encoding,
416             config = self.config, noroot = self.noroot)
417
418
419         call_retry = 0
420         try:
421             r, self.namespace = self.transport.call(self.proxy, m, ns, sa,
422                                                     encoding = self.encoding,
423                                                     http_proxy = self.http_proxy,
424                                                     config = self.config,
425                                                     timeout = self.timeout)
426
427         except socket.timeout:
428             raise SOAPTimeoutError
429
430         except Exception, ex:
431             #
432             # Call failed.
433             #
434             # See if we have a fault handling vector installed in our
435             # config. If we do, invoke it. If it returns a true value,
436             # retry the call. 
437             #
438             # In any circumstance other than the fault handler returning
439             # true, reraise the exception. This keeps the semantics of this
440             # code the same as without the faultHandler code.
441             #
442
443             if hasattr(self.config, "faultHandler"):
444                 if callable(self.config.faultHandler):
445                     call_retry = self.config.faultHandler(self.proxy, ex)
446                     if not call_retry:
447                         raise
448                 else:
449                     raise
450             else:
451                 raise
452
453         if call_retry:
454             try:
455                 r, self.namespace = self.transport.call(self.proxy, m, ns, sa,
456                                                         encoding = self.encoding,
457                                                         http_proxy = self.http_proxy,
458                                                         config = self.config,
459                                                         timeout = self.timeout)
460             except socket.timeout:
461                 raise SOAPTimeoutError
462             
463
464         p, attrs = parseSOAPRPC(r, attrs = 1)
465
466         try:
467             throw_struct = self.throw_faults and \
468                 isinstance (p, faultType)
469         except:
470             throw_struct = 0
471
472         if throw_struct:
473             if Config.debug:
474                 print p
475             raise p
476
477         # If unwrap_results=1 and there is only element in the struct,
478         # SOAPProxy will assume that this element is the result
479         # and return it rather than the struct containing it.
480         # Otherwise SOAPproxy will return the struct with all the
481         # elements as attributes.
482         if self.unwrap_results:
483             try:
484                 count = 0
485                 for i in p.__dict__.keys():
486                     if i[0] != "_":  # don't count the private stuff
487                         count += 1
488                         t = getattr(p, i)
489                 if count == 1: # Only one piece of data, bubble it up
490                     p = t 
491             except:
492                 pass
493
494         # Automatically simplfy SOAP complex types into the
495         # corresponding python types. (structType --> dict,
496         # arrayType --> array, etc.)
497         if self.simplify_objects:
498             p = simplify(p)
499
500         if self.config.returnAllAttrs:
501             return p, attrs
502         return p
503
504     def _callWithBody(self, body):
505         return self.__call(None, body, {})
506
507     def __getattr__(self, name):  # hook to catch method calls
508         if name in ( '__del__', '__getinitargs__', '__getnewargs__',
509            '__getstate__', '__setstate__', '__reduce__', '__reduce_ex__'):
510             raise AttributeError, name
511         return self.__Method(self.__call, name, config = self.config)
512
513     # To handle attribute weirdness
514     class __Method:
515         # Some magic to bind a SOAP method to an RPC server.
516         # Supports "nested" methods (e.g. examples.getStateName) -- concept
517         # borrowed from xmlrpc/soaplib -- www.pythonware.com
518         # Altered (improved?) to let you inline namespaces on a per call
519         # basis ala SOAP::LITE -- www.soaplite.com
520
521         def __init__(self, call, name, ns = None, sa = None, hd = None,
522             ma = None, config = Config):
523
524             self.__call         = call
525             self.__name         = name
526             self.__ns           = ns
527             self.__sa           = sa
528             self.__hd           = hd
529             self.__ma           = ma
530             self.__config       = config
531             return
532
533         def __call__(self, *args, **kw):
534             if self.__name[0] == "_":
535                 if self.__name in ["__repr__","__str__"]:
536                     return self.__repr__()
537                 else:
538                     return self.__f_call(*args, **kw)
539             else:
540                 return self.__r_call(*args, **kw)
541                         
542         def __getattr__(self, name):
543             if name == '__del__':
544                 raise AttributeError, name
545             if self.__name[0] == "_":
546                 # Don't nest method if it is a directive
547                 return self.__class__(self.__call, name, self.__ns,
548                     self.__sa, self.__hd, self.__ma)
549
550             return self.__class__(self.__call, "%s.%s" % (self.__name, name),
551                 self.__ns, self.__sa, self.__hd, self.__ma)
552
553         def __f_call(self, *args, **kw):
554             if self.__name == "_ns": self.__ns = args
555             elif self.__name == "_sa": self.__sa = args
556             elif self.__name == "_hd": self.__hd = args
557             elif self.__name == "_ma": self.__ma = args
558             return self
559
560         def __r_call(self, *args, **kw):
561             return self.__call(self.__name, args, kw, self.__ns, self.__sa,
562                 self.__hd, self.__ma)
563
564         def __repr__(self):
565             return "<%s at %d>" % (self.__class__, id(self))