1 | #!/usr/bin/python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | # This program is free software; you can redistribute it and/or modify |
---|
4 | # it under the terms of the GNU Lesser General Public License as published by the |
---|
5 | # Free Software Foundation; either version 3, or (at your option) any later |
---|
6 | # version. |
---|
7 | # |
---|
8 | # This program is distributed in the hope that it will be useful, but |
---|
9 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
---|
10 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
---|
11 | # for more details. |
---|
12 | |
---|
13 | """Pythonic simple SOAP Client transport""" |
---|
14 | |
---|
15 | |
---|
16 | import logging |
---|
17 | import ssl |
---|
18 | import sys |
---|
19 | try: |
---|
20 | import urllib2 |
---|
21 | from cookielib import CookieJar |
---|
22 | except ImportError: |
---|
23 | from urllib import request as urllib2 |
---|
24 | from http.cookiejar import CookieJar |
---|
25 | |
---|
26 | from . import __author__, __copyright__, __license__, __version__, TIMEOUT |
---|
27 | from .simplexml import SimpleXMLElement, TYPE_MAP, Struct |
---|
28 | |
---|
29 | log = logging.getLogger(__name__) |
---|
30 | |
---|
31 | # |
---|
32 | # Socket wrapper to enable socket.TCP_NODELAY - this greatly speeds up transactions in Linux |
---|
33 | # WARNING: this will modify the standard library socket module, use with care! |
---|
34 | # TODO: implement this as a transport faciliy |
---|
35 | # (to pass options directly to httplib2 or pycurl) |
---|
36 | # be aware of metaclasses and socks.py (SocksiPy) used by httplib2 |
---|
37 | |
---|
38 | if False: |
---|
39 | import socket |
---|
40 | realsocket = socket.socket |
---|
41 | def socketwrap(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0): |
---|
42 | sockobj = realsocket(family, type, proto) |
---|
43 | if type == socket.SOCK_STREAM: |
---|
44 | sockobj.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) |
---|
45 | return sockobj |
---|
46 | socket.socket = socketwrap |
---|
47 | |
---|
48 | # |
---|
49 | # We store metadata about what available transport mechanisms we have available. |
---|
50 | # |
---|
51 | _http_connectors = {} # libname: classimpl mapping |
---|
52 | _http_facilities = {} # functionalitylabel: [sequence of libname] mapping |
---|
53 | |
---|
54 | |
---|
55 | class TransportBase: |
---|
56 | @classmethod |
---|
57 | def supports_feature(cls, feature_name): |
---|
58 | return cls._wrapper_name in _http_facilities[feature_name] |
---|
59 | |
---|
60 | # |
---|
61 | # httplib2 support. |
---|
62 | # |
---|
63 | try: |
---|
64 | import httplib2 |
---|
65 | if sys.version > '3' and httplib2.__version__ <= "0.7.7": |
---|
66 | import http.client |
---|
67 | # httplib2 workaround: check_hostname needs a SSL context with either |
---|
68 | # CERT_OPTIONAL or CERT_REQUIRED |
---|
69 | # see https://code.google.com/p/httplib2/issues/detail?id=173 |
---|
70 | orig__init__ = http.client.HTTPSConnection.__init__ |
---|
71 | def fixer(self, host, port, key_file, cert_file, timeout, context, |
---|
72 | check_hostname, *args, **kwargs): |
---|
73 | chk = kwargs.get('disable_ssl_certificate_validation', True) ^ True |
---|
74 | orig__init__(self, host, port=port, key_file=key_file, |
---|
75 | cert_file=cert_file, timeout=timeout, context=context, |
---|
76 | check_hostname=chk) |
---|
77 | http.client.HTTPSConnection.__init__ = fixer |
---|
78 | except ImportError: |
---|
79 | TIMEOUT = None # timeout not supported by urllib2 |
---|
80 | pass |
---|
81 | else: |
---|
82 | class Httplib2Transport(httplib2.Http, TransportBase): |
---|
83 | _wrapper_version = "httplib2 %s" % httplib2.__version__ |
---|
84 | _wrapper_name = 'httplib2' |
---|
85 | |
---|
86 | def __init__(self, timeout, proxy=None, cacert=None, sessions=False): |
---|
87 | # httplib2.debuglevel=4 |
---|
88 | kwargs = {} |
---|
89 | if proxy: |
---|
90 | import socks |
---|
91 | kwargs['proxy_info'] = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, **proxy) |
---|
92 | log.info("using proxy %s" % proxy) |
---|
93 | |
---|
94 | # set optional parameters according to supported httplib2 version |
---|
95 | if httplib2.__version__ >= '0.3.0': |
---|
96 | kwargs['timeout'] = timeout |
---|
97 | if httplib2.__version__ >= '0.7.0': |
---|
98 | kwargs['disable_ssl_certificate_validation'] = cacert is None |
---|
99 | kwargs['ca_certs'] = cacert |
---|
100 | httplib2.Http.__init__(self, **kwargs) |
---|
101 | |
---|
102 | _http_connectors['httplib2'] = Httplib2Transport |
---|
103 | _http_facilities.setdefault('proxy', []).append('httplib2') |
---|
104 | _http_facilities.setdefault('cacert', []).append('httplib2') |
---|
105 | |
---|
106 | import inspect |
---|
107 | if 'timeout' in inspect.getargspec(httplib2.Http.__init__)[0]: |
---|
108 | _http_facilities.setdefault('timeout', []).append('httplib2') |
---|
109 | |
---|
110 | |
---|
111 | # |
---|
112 | # urllib2 support. |
---|
113 | # |
---|
114 | class urllib2Transport(TransportBase): |
---|
115 | _wrapper_version = "urllib2 %s" % urllib2.__version__ |
---|
116 | _wrapper_name = 'urllib2' |
---|
117 | |
---|
118 | def __init__(self, timeout=None, proxy=None, cacert=None, sessions=False): |
---|
119 | if (timeout is not None) and not self.supports_feature('timeout'): |
---|
120 | raise RuntimeError('timeout is not supported with urllib2 transport') |
---|
121 | if proxy: |
---|
122 | raise RuntimeError('proxy is not supported with urllib2 transport') |
---|
123 | if cacert: |
---|
124 | raise RuntimeError('cacert is not support with urllib2 transport') |
---|
125 | |
---|
126 | handlers = [] |
---|
127 | |
---|
128 | if ((sys.version_info[0] == 2 and sys.version_info >= (2,7,9)) or |
---|
129 | (sys.version_info[0] == 3 and sys.version_info >= (3,2,0))): |
---|
130 | context = ssl.create_default_context() |
---|
131 | context.check_hostname = False |
---|
132 | context.verify_mode = ssl.CERT_NONE |
---|
133 | handlers.append(urllib2.HTTPSHandler(context=context)) |
---|
134 | |
---|
135 | if sessions: |
---|
136 | handlers.append(urllib2.HTTPCookieProcessor(CookieJar())) |
---|
137 | |
---|
138 | opener = urllib2.build_opener(*handlers) |
---|
139 | self.request_opener = opener.open |
---|
140 | self._timeout = timeout |
---|
141 | |
---|
142 | def request(self, url, method="GET", body=None, headers={}): |
---|
143 | req = urllib2.Request(url, body, headers) |
---|
144 | try: |
---|
145 | f = self.request_opener(req, timeout=self._timeout) |
---|
146 | return f.info(), f.read() |
---|
147 | except urllib2.HTTPError as f: |
---|
148 | if f.code != 500: |
---|
149 | raise |
---|
150 | return f.info(), f.read() |
---|
151 | |
---|
152 | _http_connectors['urllib2'] = urllib2Transport |
---|
153 | _http_facilities.setdefault('sessions', []).append('urllib2') |
---|
154 | |
---|
155 | if sys.version_info >= (2, 6): |
---|
156 | _http_facilities.setdefault('timeout', []).append('urllib2') |
---|
157 | |
---|
158 | # |
---|
159 | # pycurl support. |
---|
160 | # experimental: pycurl seems faster + better proxy support (NTLM) + ssl features |
---|
161 | # |
---|
162 | try: |
---|
163 | import pycurl |
---|
164 | except ImportError: |
---|
165 | pass |
---|
166 | else: |
---|
167 | try: |
---|
168 | from cStringIO import StringIO |
---|
169 | except ImportError: |
---|
170 | try: |
---|
171 | from StringIO import StringIO |
---|
172 | except ImportError: |
---|
173 | from io import StringIO |
---|
174 | |
---|
175 | class pycurlTransport(TransportBase): |
---|
176 | _wrapper_version = pycurl.version |
---|
177 | _wrapper_name = 'pycurl' |
---|
178 | |
---|
179 | def __init__(self, timeout, proxy=None, cacert=None, sessions=False): |
---|
180 | self.timeout = timeout |
---|
181 | self.proxy = proxy or {} |
---|
182 | self.cacert = cacert |
---|
183 | |
---|
184 | def request(self, url, method, body, headers): |
---|
185 | c = pycurl.Curl() |
---|
186 | c.setopt(pycurl.URL, url) |
---|
187 | if 'proxy_host' in self.proxy: |
---|
188 | c.setopt(pycurl.PROXY, self.proxy['proxy_host']) |
---|
189 | if 'proxy_port' in self.proxy: |
---|
190 | c.setopt(pycurl.PROXYPORT, self.proxy['proxy_port']) |
---|
191 | if 'proxy_user' in self.proxy: |
---|
192 | c.setopt(pycurl.PROXYUSERPWD, "%(proxy_user)s:%(proxy_pass)s" % self.proxy) |
---|
193 | self.buf = StringIO() |
---|
194 | c.setopt(pycurl.WRITEFUNCTION, self.buf.write) |
---|
195 | #c.setopt(pycurl.READFUNCTION, self.read) |
---|
196 | #self.body = StringIO(body) |
---|
197 | #c.setopt(pycurl.HEADERFUNCTION, self.header) |
---|
198 | if self.cacert: |
---|
199 | c.setopt(c.CAINFO, self.cacert) |
---|
200 | c.setopt(pycurl.SSL_VERIFYPEER, self.cacert and 1 or 0) |
---|
201 | c.setopt(pycurl.SSL_VERIFYHOST, self.cacert and 2 or 0) |
---|
202 | c.setopt(pycurl.CONNECTTIMEOUT, self.timeout) |
---|
203 | c.setopt(pycurl.TIMEOUT, self.timeout) |
---|
204 | if method == 'POST': |
---|
205 | c.setopt(pycurl.POST, 1) |
---|
206 | c.setopt(pycurl.POSTFIELDS, body) |
---|
207 | if headers: |
---|
208 | hdrs = ['%s: %s' % (k, v) for k, v in headers.items()] |
---|
209 | log.debug(hdrs) |
---|
210 | c.setopt(pycurl.HTTPHEADER, hdrs) |
---|
211 | c.perform() |
---|
212 | c.close() |
---|
213 | return {}, self.buf.getvalue() |
---|
214 | |
---|
215 | _http_connectors['pycurl'] = pycurlTransport |
---|
216 | _http_facilities.setdefault('proxy', []).append('pycurl') |
---|
217 | _http_facilities.setdefault('cacert', []).append('pycurl') |
---|
218 | _http_facilities.setdefault('timeout', []).append('pycurl') |
---|
219 | |
---|
220 | |
---|
221 | class DummyTransport: |
---|
222 | """Testing class to load a xml response""" |
---|
223 | |
---|
224 | def __init__(self, xml_response): |
---|
225 | self.xml_response = xml_response |
---|
226 | |
---|
227 | def request(self, location, method, body, headers): |
---|
228 | log.debug("%s %s", method, location) |
---|
229 | log.debug(headers) |
---|
230 | log.debug(body) |
---|
231 | return {}, self.xml_response |
---|
232 | |
---|
233 | |
---|
234 | def get_http_wrapper(library=None, features=[]): |
---|
235 | # If we are asked for a specific library, return it. |
---|
236 | if library is not None: |
---|
237 | try: |
---|
238 | return _http_connectors[library] |
---|
239 | except KeyError: |
---|
240 | raise RuntimeError('%s transport is not available' % (library,)) |
---|
241 | |
---|
242 | # If we haven't been asked for a specific feature either, then just return our favourite |
---|
243 | # implementation. |
---|
244 | if not features: |
---|
245 | return _http_connectors.get('httplib2', _http_connectors['urllib2']) |
---|
246 | |
---|
247 | # If we are asked for a connector which supports the given features, then we will |
---|
248 | # try that. |
---|
249 | current_candidates = _http_connectors.keys() |
---|
250 | new_candidates = [] |
---|
251 | for feature in features: |
---|
252 | for candidate in current_candidates: |
---|
253 | if candidate in _http_facilities.get(feature, []): |
---|
254 | new_candidates.append(candidate) |
---|
255 | current_candidates = new_candidates |
---|
256 | new_candidates = [] |
---|
257 | |
---|
258 | # Return the first candidate in the list. |
---|
259 | try: |
---|
260 | candidate_name = current_candidates[0] |
---|
261 | except IndexError: |
---|
262 | raise RuntimeError("no transport available which supports these features: %s" % (features,)) |
---|
263 | else: |
---|
264 | return _http_connectors[candidate_name] |
---|
265 | |
---|
266 | |
---|
267 | def set_http_wrapper(library=None, features=[]): |
---|
268 | """Set a suitable HTTP connection wrapper.""" |
---|
269 | global Http |
---|
270 | Http = get_http_wrapper(library, features) |
---|
271 | return Http |
---|
272 | |
---|
273 | |
---|
274 | def get_Http(): |
---|
275 | """Return current transport class""" |
---|
276 | global Http |
---|
277 | return Http |
---|
278 | |
---|
279 | |
---|
280 | # define the default HTTP connection class (it can be changed at runtime!): |
---|
281 | set_http_wrapper() |
---|