2 # -*- encoding: utf-8 -*-
4 # Copyright P. Christeas <p_christ@hol.gr> 2008,2009
5 # Copyright OpenERP SA. (http://www.openerp.com) 2010
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 ###############################################################################
30 """ A trivial HTTP/WebDAV client, used for testing the server
32 # code taken from the 'http-client.py' script:
33 # http://git.hellug.gr/?p=xrg/openerp;a=history;f=tests/http-client.py;hb=refs/heads/xrg-60
38 import xml.dom.minidom
42 from tools import config
43 from xmlrpclib import Transport, ProtocolError
47 log = logging.getLogger('http-client')
49 class HTTP11(httplib.HTTP):
51 _http_vsn_str = 'HTTP/1.1'
53 class PersistentTransport(Transport):
54 """Handles an HTTP transaction to an XML-RPC server, persistently."""
56 def __init__(self, use_datetime=0):
57 self._use_datetime = use_datetime
59 log.debug("Using persistent transport")
61 def make_connection(self, host):
62 # create a HTTP connection object from a host descriptor
63 if not self._http.has_key(host):
64 host, extra_headers, x509 = self.get_host_info(host)
65 self._http[host] = HTTP11(host)
66 log.debug("New connection to %s", host)
67 return self._http[host]
69 def get_host_info(self, host):
70 host, extra_headers, x509 = Transport.get_host_info(self,host)
71 if extra_headers == None:
74 extra_headers.append( ( 'Connection', 'keep-alive' ))
76 return host, extra_headers, x509
78 def _parse_response(self, file, sock, response):
79 """ read response from input file/socket, and parse it
80 We are persistent, so it is important to only parse
81 the right amount of input
84 p, u = self.getparser()
86 if response.msg.get('content-encoding') == 'gzip':
87 gzdata = StringIO.StringIO()
88 while not response.isclosed():
89 rdata = response.read(1024)
94 rbuffer = gzip.GzipFile(mode='rb', fileobj=gzdata)
96 respdata = rbuffer.read()
101 while not response.isclosed():
102 rdata = response.read(1024)
112 def request(self, host, handler, request_body, verbose=0):
113 # issue XML-RPC request
115 h = self.make_connection(host)
119 self.send_request(h, handler, request_body)
120 self.send_host(h, host)
121 self.send_user_agent(h)
122 self.send_content(h, request_body)
124 resp = h._conn.getresponse()
125 # TODO: except BadStatusLine, e:
127 errcode, errmsg, headers = resp.status, resp.reason, resp.msg
137 self.verbose = verbose
141 except AttributeError:
144 return self._parse_response(h.getfile(), sock, resp)
146 class CompressedTransport(PersistentTransport):
147 def send_content(self, connection, request_body):
148 connection.putheader("Content-Type", "text/xml")
150 if len(request_body) > 512 or True:
151 buffer = StringIO.StringIO()
152 output = gzip.GzipFile(mode='wb', fileobj=buffer)
153 output.write(request_body)
156 request_body = buffer.getvalue()
157 connection.putheader('Content-Encoding', 'gzip')
159 connection.putheader("Content-Length", str(len(request_body)))
160 connection.putheader("Accept-Encoding",'gzip')
161 connection.endheaders()
163 connection.send(request_body)
165 def send_request(self, connection, handler, request_body):
166 connection.putrequest("POST", handler, skip_accept_encoding=1)
168 class SafePersistentTransport(PersistentTransport):
169 def make_connection(self, host):
170 # create a HTTP connection object from a host descriptor
171 if not self._http.has_key(host):
172 host, extra_headers, x509 = self.get_host_info(host)
173 self._http[host] = httplib.HTTPS(host, None, **(x509 or {}))
174 log.debug("New connection to %s", host)
175 return self._http[host]
177 class AuthClient(object):
178 def getAuth(self, atype, realm):
179 raise NotImplementedError("Cannot authenticate for %s" % atype)
181 def resolveFailedRealm(self, realm):
182 """ Called when, using a known auth type, the realm is not in cache
184 raise NotImplementedError("Cannot authenticate for realm %s" % realm)
186 class BasicAuthClient(AuthClient):
188 self._realm_dict = {}
190 def getAuth(self, atype, realm):
191 if atype != 'Basic' :
192 return super(BasicAuthClient,self).getAuth(atype, realm)
194 if not self._realm_dict.has_key(realm):
195 log.debug("realm dict: %r", self._realm_dict)
196 log.debug("missing key: \"%s\"" % realm)
197 self.resolveFailedRealm(realm)
198 return 'Basic '+ self._realm_dict[realm]
200 def addLogin(self, realm, username, passwd):
201 """ Add some known username/password for a specific login.
202 This function should be called once, for each realm
203 that we want to authenticate against
206 auths = base64.encodestring(username + ':' + passwd)
207 if auths[-1] == "\n":
209 self._realm_dict[realm] = auths
211 class addAuthTransport:
212 """ Intermediate class that authentication algorithm to http transport
215 def setAuthClient(self, authobj):
216 """ Set the authentication client object.
217 This method must be called before any request is issued, that
218 would require http authentication
220 assert isinstance(authobj, AuthClient)
221 self._auth_client = authobj
224 def request(self, host, handler, request_body, verbose=0):
225 # issue XML-RPC request
227 h = self.make_connection(host)
236 self.send_request(h, handler, request_body)
237 self.send_host(h, host)
238 self.send_user_agent(h)
240 # This line will bork if self.setAuthClient has not
241 # been issued. That is a programming error, fix your code!
242 auths = self._auth_client.getAuth(atype, realm)
243 log.debug("sending authorization: %s", auths)
244 h.putheader('Authorization', auths)
245 self.send_content(h, request_body)
247 resp = h._conn.getresponse()
248 # except BadStatusLine, e:
251 if resp.status == 401:
252 if 'www-authenticate' in resp.msg:
253 (atype,realm) = resp.msg.getheader('www-authenticate').split(' ',1)
255 if realm.startswith('realm="') and realm.endswith('"'):
257 log.debug("Resp: %r %r", resp.version,resp.isclosed(), resp.will_close)
258 log.debug("Want to do auth %s for realm %s", atype, realm)
260 raise ProtocolError(host+handler, 403,
261 "Unknown authentication method: %s" % atype, resp.msg)
262 continue # with the outer while loop
264 raise ProtocolError(host+handler, 403,
265 'Server-incomplete authentication', resp.msg)
267 if resp.status != 200:
268 raise ProtocolError( host + handler,
269 resp.status, resp.reason, resp.msg )
271 self.verbose = verbose
275 except AttributeError:
278 return self._parse_response(h.getfile(), sock, resp)
280 raise ProtocolError(host+handler, 403, "No authentication",'')
282 class PersistentAuthTransport(addAuthTransport,PersistentTransport):
285 class PersistentAuthCTransport(addAuthTransport,CompressedTransport):
288 class HTTPSConnection(httplib.HTTPSConnection):
291 "Connect to a host on a given (SSL) port. check the certificate"
294 if HTTPSConnection.certs_file:
295 ca_certs = HTTPSConnection.certs_file
296 cert_reqs = ssl.CERT_REQUIRED
299 cert_reqs = ssl.CERT_NONE
300 sock = socket.create_connection((self.host, self.port), self.timeout)
301 self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
306 def getpeercert(self):
310 cert = self.sock.getpeercert()
312 cert = ssl.get_server_certificate((self.host,self.port),
313 ssl_version=ssl.PROTOCOL_SSLv23 )
314 lf = (len(ssl.PEM_FOOTER)+1)
315 if cert[0-lf] != '\n':
316 cert = cert[:0-lf]+'\n'+cert[0-lf:]
317 log.debug("len-footer: %s cert: %r", lf, cert[0-lf])
322 class DAVClient(object):
323 """An instance of a WebDAV client, connected to the OpenERP server
326 def __init__(self, user=None, passwd=None, dbg=0, use_ssl=False):
328 self.host = config.get_misc('httpsd', 'interface', False)
329 self.port = config.get_misc('httpsd', 'port', 8071)
331 self.host = config.get('xmlrpcs_interface')
332 self.port = config.get('xmlrpcs_port')
334 self.host = config.get_misc('httpd', 'interface')
335 self.port = config.get_misc('httpd', 'port', 8069)
337 self.host = config.get('xmlrpc_interface')
338 self.port = config.get('xmlrpc_port') or self.port
339 if self.host == '0.0.0.0' or not self.host:
340 self.host = '127.0.0.1'
341 self.port = int(self.port)
342 if not config.get_misc('webdav','enable',True):
343 raise Exception("WebDAV is disabled, cannot continue")
344 self.davpath = '/' + config.get_misc('webdav','vdir','webdav')
349 def get_creds(self, obj, cr, uid):
350 """Read back the user credentials from cr, uid
352 @param obj is any orm object, in order to use its pool
353 @param uid is the numeric id, which we will try to reverse resolve
355 note: this is a hackish way to get the credentials. It is expected
356 to break if "base_crypt" is used.
358 ruob = obj.pool.get('res.users')
359 res = ruob.read(cr, 1, [uid,], ['login', 'password'])
360 assert res, "uid %s not found" % uid
361 self.user = res[0]['login']
362 self.passwd = res[0]['password']
365 def _http_request(self, path, method='GET', hdrs=None, body=None):
370 log.debug("Getting %s http://%s:%d/%s", method, self.host, self.port, path)
371 conn = httplib.HTTPConnection(self.host, port=self.port)
372 conn.set_debuglevel(dbg)
375 if not hdrs.has_key('Connection'):
376 hdrs['Connection']= 'keep-alive'
377 conn.request(method, path, body, hdrs )
379 r1 = conn.getresponse()
380 except httplib.BadStatusLine, bsl:
381 log.warning("Bad status line: %s", bsl.line)
382 raise Exception('Bad status line')
383 if r1.status == 401: # and r1.headers:
384 if 'www-authenticate' in r1.msg:
385 (atype,realm) = r1.msg.getheader('www-authenticate').split(' ',1)
388 raise Exception('Must auth, have no user/pass!')
389 log.debug("Ver: %s, closed: %s, will close: %s", r1.version,r1.isclosed(), r1.will_close)
390 log.debug("Want to do auth %s for realm %s", atype, realm)
391 if atype == 'Basic' :
392 auths = base64.encodestring(self.user + ':' + self.passwd)
393 if auths[-1] == "\n":
395 hdrs['Authorization']= 'Basic '+ auths
397 conn.request(method, path, body, hdrs )
398 r1 = conn.getresponse()
400 raise Exception("Unknown auth type %s" %atype)
402 log.warning("Got 401, cannot auth")
403 raise Exception('No auth')
405 log.debug("Reponse: %s %s",r1.status, r1.reason)
407 log.debug("Body:\n%s\nEnd of body", data1)
409 ctype = r1.msg.getheader('content-type')
410 if ctype and ';' in ctype:
411 ctype, encoding = ctype.split(';',1)
412 if ctype == 'text/xml':
413 doc = xml.dom.minidom.parseString(data1)
414 log.debug("XML Body:\n %s", doc.toprettyxml(indent="\t"))
416 log.warning("could not print xml", exc_info=True)
419 return r1.status, r1.msg, data1
421 def _assert_headers(self, expect, msg):
422 """ Assert that the headers in msg contain the expect values
424 for k, v in expect.items():
425 hval = msg.getheader(k)
427 raise AssertionError("Header %s not defined in http response" % k)
428 if isinstance(v, (list, tuple)):
430 hits = map(str.strip, hval.split(delim))
436 raise AssertionError("HTTP header \"%s\" is missing: %s" %(k, ', '.join(mvits)))
438 if hval.strip() != v.strip():
439 raise AssertionError("HTTP header \"%s: %s\"" % (k, hval))
441 def gd_options(self, path='*', expect=None):
442 """ Test the http options functionality
443 If a dictionary is defined in expect, those options are
447 path = self.davpath + path
448 hdrs = { 'Content-Length': 0
450 s, m, d = self._http_request(path, method='OPTIONS', hdrs=hdrs)
451 assert s == 200, "Status: %r" % s
452 assert 'OPTIONS' in m.getheader('Allow')
453 log.debug('Options: %r', m.getheader('Allow'))
456 self._assert_headers(expect, m)