From d1daa789353d54f09bb61e9a74ad5c44e1ec97bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 6 Aug 2011 14:30:52 +0200 Subject: [PATCH] Use JSON-RPC --- CHANGELOG | 1 + tryton/common/common.py | 23 +- tryton/config.py | 2 +- tryton/exceptions.py | 13 +- tryton/fingerprints.py | 12 +- tryton/gui/main.py | 16 +- tryton/gui/window/dbcreate.py | 6 +- tryton/gui/window/dbdumpdrop.py | 2 +- tryton/gui/window/dblogin.py | 8 +- tryton/gui/window/dbrestore.py | 2 +- .../window/view_form/view/form_gtk/float.py | 10 +- .../view_form/view/form_gtk/progressbar.py | 4 +- .../window/view_form/view/list_gtk/parser.py | 18 +- .../window/view_form/widget_search/float.py | 2 +- tryton/jsonrpc.py | 272 ++++++++++++++++++ tryton/pysocket.py | 255 ---------------- tryton/rpc.py | 204 +++++-------- 17 files changed, 386 insertions(+), 464 deletions(-) create mode 100644 tryton/jsonrpc.py delete mode 100644 tryton/pysocket.py diff --git a/CHANGELOG b/CHANGELOG index acafd66790..83e1a1ad67 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Use JSON-RPC * Limit size of field when possible * Add xalign and yalign as fields attributes * Convert many popup to be no-modal diff --git a/tryton/common/common.py b/tryton/common/common.py index f2608dd686..d1447a86df 100644 --- a/tryton/common/common.py +++ b/tryton/common/common.py @@ -200,7 +200,7 @@ def request_server(server_widget): xoptions=gtk.FILL) entry_port = gtk.Entry() entry_port.set_max_length(5) - entry_port.set_text("8070") + entry_port.set_text("8000") entry_port.set_activates_default(True) entry_port.set_width_chars(16) table.attach(entry_port, 1, 2, 1, 2, yoptions=False, @@ -966,10 +966,9 @@ def to_xml(string): PLOCK = Lock() def process_exception(exception, *args): - global _USERNAME, _DATABASE, _SOCK if isinstance(exception, TrytonError): - if exception.ex_type == 'BadFingerprint': + if exception.faultCode == 'BadFingerprint': warning( _('The server fingerprint has changed since last connection!\n' 'The application will stop connecting to this server ' @@ -977,13 +976,13 @@ def process_exception(exception, *args): from tryton.gui.main import Main Main.sig_quit() sys.exit() - elif exception.ex_type == 'NotLogged': - if not rpc._SOCK: + elif exception.faultCode == 'NotLogged': + if rpc.CONNECTION is None: message(_('Connection error!\n' \ 'Unable to connect to the server!')) return False elif isinstance(exception, TrytonServerError): - if exception.ex_type == 'UserWarning': + if exception.faultCode == 'UserWarning': msg = '' if len(exception.args) > 4: msg = exception.args[3] @@ -1005,13 +1004,13 @@ def process_exception(exception, *args): return process_exception(exception, *args) return True return False - elif exception.ex_type == 'UserError': + elif exception.faultCode == 'UserError': msg = '' if len(exception.args) > 3: msg = exception.args[2] warning(str(msg), str(exception.args[1])) return False - elif exception.ex_type == 'ConcurrencyException': + elif exception.faultCode == 'ConcurrencyException': if len(args) >= 6: if concurrency(args[1], args[3][0], args[5]): if '_timestamp' in args[5]: @@ -1024,11 +1023,11 @@ def process_exception(exception, *args): else: message(_('Concurrency Exception'), msg_type=gtk.MESSAGE_ERROR) return False - elif exception.ex_type == 'NotLogged': + elif exception.faultCode == 'NotLogged': if not PLOCK.acquire(False): return False - hostname = rpc._SOCK.hostname - port = rpc._SOCK.port + hostname = rpc._HOST + port = rpc._PORT try: while True: password = ask(_('Password:'), visibility=False) @@ -1060,7 +1059,7 @@ def process_exception(exception, *args): return False if isinstance(exception, TrytonServerError): - error_title, error_detail = exception.ex_type, exception.traceback + error_title, error_detail = exception.faultCode, exception.faultString else: error_title = str(exception) error_detail = traceback.format_exc() diff --git a/tryton/config.py b/tryton/config.py index bfcc536abf..a6d5dc2dbc 100644 --- a/tryton/config.py +++ b/tryton/config.py @@ -57,7 +57,7 @@ class ConfigManager(object): 'login.profile': demo_server, 'login.login': 'demo', 'login.server': demo_server, - 'login.port': '8070', + 'login.port': '8000', 'login.db': demo_database, 'login.expanded': False, 'tip.autostart': False, diff --git a/tryton/exceptions.py b/tryton/exceptions.py index 7fa047a08e..6807420c6c 100644 --- a/tryton/exceptions.py +++ b/tryton/exceptions.py @@ -1,16 +1,11 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. +from jsonrpc import Fault - -class TrytonServerError(Exception): - - def __init__(self, *args): - super(TrytonServerError, self).__init__(*args) - self.ex_type = str(args[0]) - self.traceback = args[-1] +TrytonServerError = Fault class TrytonError(Exception): - def __init__(self, ex_type): - self.ex_type = ex_type + def __init__(self, faultCode): + self.faultCode = faultCode diff --git a/tryton/fingerprints.py b/tryton/fingerprints.py index f6e5b04f96..01d1cf8e43 100644 --- a/tryton/fingerprints.py +++ b/tryton/fingerprints.py @@ -19,21 +19,19 @@ class Fingerprints(dict): for line in known_hosts: line = line.strip() try: - key, sha1 = line.split(' ') - host, port = key.rsplit(':', 1) + host, sha1 = line.split(' ') except ValueError: continue - self[(host, port)] = sha1 + self[host] = sha1 def save(self): lines = [] with open(KNOWN_HOSTS_PATH, 'w') as known_hosts: - known_hosts.writelines('%s:%s %s' % (host, port, sha1) - + os.linesep for (host, port), sha1 in self.iteritems()) + known_hosts.writelines('%s %s' % (host, sha1) + + os.linesep for host, sha1 in self.iteritems()) def __setitem__(self, key, value): - assert isinstance(key, tuple) - assert len(key) == 2 + assert isinstance(key, basestring) assert len(value) == 59 # len of formated sha1 super(Fingerprints, self).__setitem__(key, value) self.save() diff --git a/tryton/gui/main.py b/tryton/gui/main.py index 5a957df654..90face54a1 100644 --- a/tryton/gui/main.py +++ b/tryton/gui/main.py @@ -889,7 +889,7 @@ class Main(object): try: res = DBLogin().run() except TrytonError, exception: - if exception.ex_type == 'QueryCanceled': + if exception.faultCode == 'QueryCanceled': return False except TrytonServerError, exception: common.process_exception(exception) @@ -982,12 +982,8 @@ class Main(object): return True def refresh_ssl(self): - if rpc.SECURE: - info = '' - if hasattr(rpc._SOCK.ssl_sock, 'server'): - info = str(rpc._SOCK.ssl_sock.server()) - self.tooltips.set_tip(self.secure_img, _('SSL connection') + \ - '\n' + info) + if rpc.CONNECTION is not None and rpc.CONNECTION.ssl: + self.tooltips.set_tip(self.secure_img, _('SSL connection')) self.secure_img.show() else: self.secure_img.hide() @@ -1044,7 +1040,7 @@ class Main(object): return False self.sb_username.set_text(prefs.get('status_bar', '')) self.sb_servername.set_text('%s@%s:%d/%s' % (rpc._USERNAME, - rpc._SOCK.hostname, rpc._SOCK.port, rpc._DATABASE)) + rpc._HOST, rpc._PORT, rpc._DATABASE)) if not prefs[menu_type]: if quiet: return False @@ -1396,8 +1392,8 @@ class Main(object): + [CONFIG.defaults['login.port']])[:2] database, path = (urlp.path[1:].split('/', 1) + [None])[:2] if (not path or - hostname != rpc._SOCK.hostname or - int(port) != rpc._SOCK.port or + hostname != rpc._HOST or + int(port) != rpc._PORT or database != rpc._DATABASE): return type_, path = (path.split('/', 1) + [''])[:2] diff --git a/tryton/gui/window/dbcreate.py b/tryton/gui/window/dbcreate.py index 29e7998427..9adde3bac0 100644 --- a/tryton/gui/window/dbcreate.py +++ b/tryton/gui/window/dbcreate.py @@ -29,7 +29,7 @@ class DBCreate(object): self.entry_server_connection.modify_text(gtk.STATE_INSENSITIVE, \ gtk.gdk.color_parse(common.COLOR_SCHEMES["black"])) self.tooltips.set_tip(self.entry_server_connection,_("This is the URL of " \ - "the Tryton server. Use server 'localhost' and port '8070' if " \ + "the Tryton server. Use server 'localhost' and port '8000' if " \ "the server is installed on this computer. Click on 'Change' to " \ "change the address.")) else: @@ -50,7 +50,7 @@ class DBCreate(object): "2. Find out on which address and port it is listen.\n" \ "3. If there is a firewall between the server and this " \ "client, make shure that the server address and port " \ - "(usually 8070) are not blocked.\n" \ + "(usually 8000) are not blocked.\n" \ "Click on 'Change' to change the address.")) return state @@ -177,7 +177,7 @@ class DBCreate(object): self.entry_server_connection.unset_flags(gtk.CAN_FOCUS) self.entry_server_connection.set_editable(False) self.tooltips.set_tip(self.entry_server_connection, _("This is the URL of " \ - "the server. Use server 'localhost' and port '8070' if " \ + "the server. Use server 'localhost' and port '8000' if " \ "the server is installed on this computer. Click on 'Change' to " \ "change the address.")) self.button_server_change = gtk.Button(_("C_hange"), stock=None, diff --git a/tryton/gui/window/dbdumpdrop.py b/tryton/gui/window/dbdumpdrop.py index 21819e95c2..9c01d94bec 100644 --- a/tryton/gui/window/dbdumpdrop.py +++ b/tryton/gui/window/dbdumpdrop.py @@ -136,7 +136,7 @@ class DBBackupDrop(object): self.entry_server_connection.unset_flags(gtk.CAN_FOCUS) self.entry_server_connection.set_editable(False) self.tooltips.set_tip(self.entry_server_connection, _("This is the " \ - "URL of the server. Use server 'localhost' and port '8070' " \ + "URL of the server. Use server 'localhost' and port '8000' " \ "if the server is installed on this computer. " \ "Click on 'Change' to change the address.")) table.attach(self.entry_server_connection, 1, 2, 2, 3, diff --git a/tryton/gui/window/dblogin.py b/tryton/gui/window/dblogin.py index 6316879bad..e82ca00bc7 100644 --- a/tryton/gui/window/dblogin.py +++ b/tryton/gui/window/dblogin.py @@ -169,7 +169,7 @@ class DBListEditor(object): for entryname in ('host', 'port', 'database', 'username'): entry = getattr(self, '%s_entry' % entryname) if entryname == 'port': - entry.set_text('8070') + entry.set_text('8000') else: entry.set_text('') self.current_database = None @@ -464,13 +464,13 @@ class DBLogin(object): # Profile informations self.profile_cfg = os.path.join(get_config_dir(), 'profiles.cfg') - self.profiles = ConfigParser.SafeConfigParser({'port': '8070'}) + self.profiles = ConfigParser.SafeConfigParser({'port': '8000'}) if not os.path.exists(self.profile_cfg): short_version = '.'.join(VERSION.split('.', 2)[:2]) name = 'demo%s.tryton.org' % short_version self.profiles.add_section(name) self.profiles.set(name, 'host', name) - self.profiles.set(name, 'port', '8070') + self.profiles.set(name, 'port', '8000') self.profiles.set(name, 'database', 'demo%s' % short_version) self.profiles.set(name, 'username', 'demo') else: @@ -565,7 +565,7 @@ class DBLogin(object): if ':' in netloc: return int(netloc.split(':')[1]) else: - return 8070 + return 8000 def run(self): profile_name = CONFIG['login.profile'] diff --git a/tryton/gui/window/dbrestore.py b/tryton/gui/window/dbrestore.py index 10115494d6..6be7386542 100644 --- a/tryton/gui/window/dbrestore.py +++ b/tryton/gui/window/dbrestore.py @@ -83,7 +83,7 @@ class DBRestore(object): self.entry_server_url.set_activates_default(True) self.entry_server_url.set_width_chars(16) self.tooltips.set_tip(self.entry_server_url, _("This is the URL of " \ - "the server. Use server 'localhost' and port '8070' if " \ + "the server. Use server 'localhost' and port '8000' if " \ "the server is installed on this computer. Click on " \ "'Change' to change the address.")) table.attach(self.entry_server_url, 1, 2, 0, 1, yoptions=gtk.FILL) diff --git a/tryton/gui/window/view_form/view/form_gtk/float.py b/tryton/gui/window/view_form/view/form_gtk/float.py index 58e87dcaeb..26de08b29d 100644 --- a/tryton/gui/window/view_form/view/form_gtk/float.py +++ b/tryton/gui/window/view_form/view/form_gtk/float.py @@ -27,10 +27,7 @@ class Float(Integer): self.entry.set_text('') return False self.digits = field.attrs.get('digits', (16, 2)) - if isinstance(self.digits, str): - digits = record.expr_eval(self.digits) - else: - digits = self.digits + digits = record.expr_eval(self.digits) self.entry.set_width_chars(sum(digits)) self.entry.set_text(locale.format('%.' + str(digits[1]) + 'f', field.get(record) or 0.0, True)) @@ -54,10 +51,7 @@ class Float(Integer): if new_value in ('-', decimal_point): return - if isinstance(self.digits, str): - digits = self.record.expr_eval(self.digits) - else: - digits = self.digits + digits = self.record.expr_eval(self.digits) try: locale.atof(new_value) diff --git a/tryton/gui/window/view_form/view/form_gtk/progressbar.py b/tryton/gui/window/view_form/view/form_gtk/progressbar.py index 24618ac174..42f11d6d0e 100644 --- a/tryton/gui/window/view_form/view/form_gtk/progressbar.py +++ b/tryton/gui/window/view_form/view/form_gtk/progressbar.py @@ -28,9 +28,7 @@ class ProgressBar(WidgetInterface): self.widget.set_fraction(0.0) return False value = float(field.get(record) or 0.0) - digits = field.attrs.get('digits', (16, 2)) - if isinstance(self.digits, str): - digits = record.expr_eval(self.digits) + digits = record.expr_eval(field.attrs.get('digits', (16, 2))) self.widget.set_text(locale.format('%.' + str(digits[1]) + 'f', value, True)) self.widget.set_fraction(value / 100.0) diff --git a/tryton/gui/window/view_form/view/list_gtk/parser.py b/tryton/gui/window/view_form/view/list_gtk/parser.py index 607a0ef371..72cf574487 100644 --- a/tryton/gui/window/view_form/view/list_gtk/parser.py +++ b/tryton/gui/window/view_form/view/list_gtk/parser.py @@ -207,7 +207,8 @@ class ParserTree(ParserInterface): 'float_time'): label = gtk.Label(node_attrs['sum'] + _(': ')) label_sum = gtk.Label() - if isinstance(fields[fname].attrs.get('digits'), str): + if isinstance(fields[fname].attrs.get('digits'), + basestring): digits = 2 else: digits = fields[fname].attrs.get('digits', (16, 2))[1] @@ -456,18 +457,12 @@ class Float(Char): super(Float, self).setter(column, cell, store, iter) record = store.get_value(iter, 0) field = record[self.field_name] - if isinstance(field.attrs.get('digits'), str): - digits = record.expr_eval(field.attrs['digits']) - else: - digits = field.attrs.get('digits', (16, 2)) + digits = record.expr_eval(field.attrs.get('digits', (16, 2))) cell.digits = digits def get_textual_value(self, record): field = record[self.field_name] - if isinstance(field.attrs.get('digits'), str): - digit = record.expr_eval(field.attrs['digits'])[1] - else: - digit = field.attrs.get('digits', (16, 2))[1] + digit = record.expr_eval(field.attrs.get('digits', (16, 2)))[1] return locale.format('%.'+str(digit)+'f', record[self.field_name].get_client(record) or 0.0, True) @@ -870,10 +865,7 @@ class ProgressBar(object): record = store.get_value(iter, 0) value = float(self.get_textual_value(record) or 0.0) cell.set_property('value', value) - if isinstance(self.attrs.get('digits'), str): - digit = record.expr_eval(self.attrs['digits'])[1] - else: - digit = self.attrs.get('digits', (16, 2))[1] + digit = record.expr_eval(self.attrs.get('digits', (16, 2)))[1] text = locale.format('%.' + str(digit) + 'f', value, True) cell.set_property('text', text + '%') diff --git a/tryton/gui/window/view_form/widget_search/float.py b/tryton/gui/window/view_form/widget_search/float.py index c6cdd40305..ad074338c9 100644 --- a/tryton/gui/window/view_form/widget_search/float.py +++ b/tryton/gui/window/view_form/widget_search/float.py @@ -13,7 +13,7 @@ class Float(Integer): def __init__(self, name, attrs=None, context=None, on_change=None): super(Float, self).__init__(name, attrs=attrs, context=context, on_change=on_change) - if isinstance(self.attrs.get('digits'), str): + if isinstance(self.attrs.get('digits'), basestring): self.digits = (16, 2) else: self.digits = self.attrs.get('digits', (16, 2)) diff --git a/tryton/jsonrpc.py b/tryton/jsonrpc.py new file mode 100644 index 0000000000..0bd07daa6c --- /dev/null +++ b/tryton/jsonrpc.py @@ -0,0 +1,272 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +import xmlrpclib +import json +import ssl +import httplib +from decimal import Decimal +import datetime +import socket +import gzip +import StringIO +import hashlib +import sys + +__all__ = ["ResponseError", "Fault", "ProtocolError", "Transport", + "ServerProxy"] + + +class ResponseError(xmlrpclib.ResponseError): + pass + + +class Fault(xmlrpclib.Fault): + pass + + +class ProtocolError(xmlrpclib.ProtocolError): + pass + +def object_hook(dct): + if '__class__' in dct: + if dct['__class__'] == 'datetime': + return datetime.datetime(dct['year'], dct['month'], dct['day'], + dct['hour'], dct['minute'], dct['second']) + elif dct['__class__'] == 'date': + return datetime.date(dct['year'], dct['month'], dct['day']) + elif dct['__class__'] == 'Decimal': + return Decimal(dct['decimal']) + return dct + + +class JSONEncoder(json.JSONEncoder): + + def default(self, obj): + if isinstance(obj, datetime.date): + if isinstance(obj, datetime.datetime): + return {'__class__': 'datetime', + 'year': obj.year, + 'month': obj.month, + 'day': obj.day, + 'hour': obj.hour, + 'minute': obj.minute, + 'second': obj.second, + } + return {'__class__': 'date', + 'year': obj.year, + 'month': obj.month, + 'day': obj.day, + } + elif isinstance(obj, Decimal): + return {'__class__': 'Decimal', + 'decimal': str(obj), + } + return super(JSONEncoder, self).default(obj) + + +class JSONParser(object): + + def __init__(self, target): + self.__targer = target + + def feed(self, data): + self.__targer.feed(data) + + def close(self): + pass + + +class JSONUnmarshaller(object): + data = '' + + def feed(self, data): + self.data += data + + def close(self): + return json.loads(self.data, object_hook=object_hook) + + +class Transport(xmlrpclib.Transport, xmlrpclib.SafeTransport): + + accept_gzip_encoding = True + encode_threshold = 1400 # common MTU + + def __init__(self, fingerprints=None, ca_certs=None): + xmlrpclib.Transport.__init__(self) + self.__connection = (None, None) + self.__fingerprints = fingerprints + self.__ca_certs = ca_certs + + def getparser(self): + target = JSONUnmarshaller() + parser = JSONParser(target) + return parser, target + + def get_host_info(self, host): + host, extra_headers, x509 = xmlrpclib.Transport.get_host_info(self, host) + if extra_headers is None: + extra_headers = [] + extra_headers.append(('Connection', 'keep-alive')) + return host, extra_headers, x509 + + def send_content(self, connection, request_body): + connection.putheader("Content-Type", "text/json") + if (self.encode_threshold is not None and + self.encode_threshold < len(request_body) and + gzip): + connection.putheader("Content-Encoding", "gzip") + buffer = StringIO.StringIO() + output = gzip.GzipFile(mode='wb', fileobj=buffer) + output.write(request_body) + output.close() + buffer.seek(0) + request_body = buffer.getvalue() + connection.putheader("Content-Length", str(len(request_body))) + connection.endheaders() + if request_body: + connection.send(request_body) + + def make_connection(self, host): + if self.__connection and host == self.__connection[0]: + return self.__connection[1] + fingerprint = None + host, extra_headers, x509 = self.get_host_info(host) + + ca_certs = self.__ca_certs + cert_reqs = ssl.CERT_REQUIRED if ca_certs else ssl.CERT_NONE + + + class HTTPSConnection(httplib.HTTPSConnection): + def connect(self): + sock = socket.create_connection((self.host, self.port), + self.timeout) + if self._tunnel_host: + self.sock = sock + self._tunnel() + self.sock = ssl.wrap_socket(sock, self.key_file, + self.cert_file, ca_certs=ca_certs, cert_reqs=cert_reqs) + + self.__connection = host, HTTPSConnection(host) + try: + self.__connection[1].connect() + sock = self.__connection[1].sock + try: + peercert = sock.getpeercert(True) + except socket.error: + peercert = None + def format_hash(value): + return reduce(lambda x, y: x + y[1].upper() + + ((y[0] % 2 and y[0] + 1 < len(value)) and ':' or ''), + enumerate(value), '') + fingerprint = format_hash(hashlib.sha1(peercert).hexdigest()) + except ssl.SSLError, e: + self.__connection = host, httplib.HTTPConnection(host) + if self.__fingerprints is not None: + if host in self.__fingerprints: + if self.__fingerprints[host] != fingerprint: + self.close() + raise ssl.SSLError('BadFingerprint') + elif fingerprint: + self.__fingerprints[host] = fingerprint + return self.__connection[1] + + if sys.version_info[:2] <= (2, 6): + + def request(self, host, handler, request_body, verbose=0): + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_content(h, request_body) + + response = h.getresponse() + + if response.status != 200: + raise ProtocolError( + host + handler, + response.status, + response.reason, + response.getheaders() + ) + + self.verbose = verbose + + try: + sock = h._conn.sock + except AttributeError: + sock = None + + if response.getheader("Content-Encoding", "") == "gzip": + response = gzip.GzipFile(mode="rb", + fileobj=StringIO.StringIO(response.read())) + sock = None + + return self._parse_response(response, sock) + + def send_request(self, connection, handler, request_body): + xmlrpclib.Transport.send_request(self, connection, handler, + request_body) + connection.putheader("Accept-Encoding", "gzip") + + def close(self): + if self.__connection[1]: + self.__connection[1].close() + self.__connection = (None, None) + + +class ServerProxy(xmlrpclib.ServerProxy): + __id = 0 + + def __init__(self, host, port, database='', verbose=0, + fingerprints=None, ca_certs=None): + self.__host = '%s:%s' % (host, port) + self.__handler = '/' + database + self.__transport = Transport(fingerprints, ca_certs) + self.__verbose = verbose + + def __request(self, methodname, params): + self.__id += 1 + id_ = self.__id + request = json.dumps({ + 'id': id_, + 'method': methodname, + 'params': params, + }, cls=JSONEncoder) + + try: + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + except socket.error, v: + # trap 'Broken pipe' + if v.args[0] != 32: + raise + # try one more time + self.__transport.close() + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + + if response['id'] != id_: + raise ResponseError('Invalid response id (%s) excpected %s' % + (response['id'], id_)) + if response.get('error'): + raise Fault(*response['error']) + return response['result'] + + def close(self): + self.__transport.close() + + @property + def ssl(self): + return isinstance(self.__transport.make_connection(self.__host), + httplib.HTTPSConnection) diff --git a/tryton/pysocket.py b/tryton/pysocket.py deleted file mode 100644 index 2629d79357..0000000000 --- a/tryton/pysocket.py +++ /dev/null @@ -1,255 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import socket -# can't use/fall-back pickle due to different interface :-( -import cPickle -try: - import cStringIO as StringIO -except ImportError: - import StringIO -try: - import ssl -except ImportError: - ssl = None -import gzip -try: - import hashlib -except ImportError: - hashlib = None - import sha -import os - -from tryton.exceptions import TrytonError, TrytonServerError - -DNS_CACHE = {} -MAX_SIZE = 999999999 -MAX_LENGHT = len(str(MAX_SIZE)) -CONNECT_TIMEOUT = 5 -TIMEOUT = 3600 -GZIP_THRESHOLD = 1400 # common MTU - -_ALLOWED_MODULES = {'datetime': ['datetime', 'date'], 'decimal': ['Decimal']} - -def checkfunction(module, klass): - if module in _ALLOWED_MODULES and klass in _ALLOWED_MODULES[module]: - mod = __import__(module, {}, {}, ['__all__']) - _class = getattr(mod, klass) - return _class - raise ValueError('Not supported: %s/%s' % (module, klass)) - - -class PySocket: - - def __init__(self, sock=None, fingerprints=None, ca_certs=None): - self.sock = sock - self.host = None - self.hostname = None - self.port = None - self.ssl = False - self.ssl_sock = None - self.connected = False - self.buffer = '' - self.fingerprints = fingerprints - if ca_certs and os.path.isfile(ca_certs): - self.ca_certs = ca_certs - else: - self.ca_certs = None - - def connect(self, host, port=False): - if not port: - buf = host.split('//')[1] - host, port = buf.rsplit(':', 1) - hostname = host - if host in DNS_CACHE: - host = DNS_CACHE[host] - self.sock = None - if socket.has_ipv6: - try: - socket.getaddrinfo(host, int(port), socket.AF_INET6) - self.sock = socket.socket(socket.AF_INET6, - socket.SOCK_STREAM) - self.sock.settimeout(CONNECT_TIMEOUT) - self.sock.connect((host, int(port))) - except socket.error: - self.sock = None - if self.sock is None: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.settimeout(CONNECT_TIMEOUT) - self.sock.connect((host, int(port))) - DNS_CACHE[hostname], port = self.sock.getpeername()[:2] - try: - sock = None - if socket.has_ipv6: - try: - socket.getaddrinfo(host, int(port), socket.AF_INET6) - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - sock.settimeout(CONNECT_TIMEOUT) - sock.connect((host, int(port))) - except socket.error: - sock = None - if sock is None: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(CONNECT_TIMEOUT) - sock.connect((host, int(port))) - if ssl: - ssl_sock = ssl.wrap_socket(sock) - self.ssl = True - elif hasattr(socket, 'ssl'): - ssl_sock = socket.ssl(sock) - self.ssl = True - except socket.error: - pass - self.sock.settimeout(TIMEOUT) - fingerprint = None - if self.ssl: - if ssl: - self.ssl_sock = ssl.wrap_socket(self.sock, - ca_certs=self.ca_certs, cert_reqs=(self.ca_certs and - ssl.CERT_REQUIRED or ssl.CERT_NONE)) - elif hasattr(socket, 'ssl'): - self.ssl_sock = socket.ssl(self.sock, certfile=self.ca_certs) - try: - peercert = self.ssl_sock.getpeercert(True) - except socket.error: - peercert = None - def format_hash(value): - return reduce(lambda x, y: x + y[1].upper() + - ((y[0] % 2 and y[0] + 1 < len(value)) and ':' or ''), - enumerate(value), '') - if peercert and hashlib: - fingerprint = format_hash(hashlib.sha1(peercert).hexdigest()) - elif peercert: - fingerprint = format_hash(sha1.new(peercert).hexdigest()) - self.host = host - self.hostname = hostname - self.port = port - self.connected = True - self.buffer = '' - if self.fingerprints is not None: - key = (self.hostname, str(self.port)) - if key in self.fingerprints: - if self.fingerprints[key] != fingerprint: - self.disconnect() - raise TrytonError('BadFingerprint') - elif fingerprint: - self.fingerprints[key] = fingerprint - - def disconnect(self): - try: - sock = self.sock - if self.ssl: - sock = self.ssl_sock - try: - shutdown_value = 2 - if hasattr(socket, 'SHUT_RDWR'): - shutdown_value = socket.SHUT_RDWR - if hasattr(sock, 'sock_shutdown'): - sock.sock_shutdown(shutdown_value) - else: - sock.shutdown(shutdown_value) - except socket.error: - pass - sock.close() - except socket.error: - pass - self.sock = None - self.ssl = False - self.ssl_sock = None - self.connected = False - self.buffer = '' - - def reconnect(self): - if self.hostname and self.port: - self.disconnect() - self.connect(self.hostname, self.port) - - def send(self, msg, exception=False, traceback=None): - msg = cPickle.dumps([msg, traceback], protocol=2) - gzip_p = False - if len(msg) > GZIP_THRESHOLD: - buffer = StringIO.StringIO() - output = gzip.GzipFile(mode='wb', fileobj=buffer) - output.write(msg) - output.close() - buffer.seek(0) - msg = buffer.getvalue() - gzip_p = True - size = len(msg) - msg = str(size) + ' ' + (exception and "1" or "0") \ - + (gzip_p and "1" or "0") + msg - size = len(msg) - - if self.ssl: - totalsent = 0 - while totalsent < size: - sent = self.ssl_sock.write(msg[totalsent:]) - if sent == 0: - raise RuntimeError("socket connection broken") - totalsent = totalsent + sent - else: - self.sock.sendall(msg) - - def receive(self): - buf = self.buffer - L = [] - size_remaining = MAX_LENGHT - while size_remaining: - chunk_size = min(size_remaining, 4096) - if self.ssl: - chunk = self.ssl_sock.read(chunk_size) - else: - chunk = self.sock.recv(chunk_size) - if chunk == '': - raise RuntimeError("socket connection broken") - L.append(chunk) - size_remaining -= len(chunk) - if ' ' in chunk: - break - if size_remaining < 0: - raise RuntimeError("socket connection broken") - buf += ''.join(L) - size, msg = buf.split(' ', 1) - size = int(size) - if size > MAX_SIZE: - raise RuntimeError("socket connection broken") - while len(msg) < 2: - chunk_size = min(size + 2, 4096) - if self.ssl: - msg += self.ssl_sock.read(chunk_size) - else: - msg += self.sock.recv(chunk_size) - if msg == '': - raise RuntimeError("socket connection broken") - exception = msg[0] != "0" - gzip_p = msg[1] != "0" - L = [msg[2:]] - size_remaining = size - len(L[0]) - while size_remaining: - chunk_size = min(size_remaining, 4096) - if self.ssl: - chunk = self.ssl_sock.read(chunk_size) - else: - chunk = self.sock.recv(chunk_size) - if chunk == '': - raise RuntimeError("socket connection broken") - L.append(chunk) - size_remaining -= len(chunk) - msg = ''.join(L) - if len(msg) > size: - self.buffer = msg[size:] - msg = msg[:size] - else: - self.buffer = '' - msgio = StringIO.StringIO(msg) - if gzip_p: - output = gzip.GzipFile(mode='r', fileobj=msgio) - msgio = StringIO.StringIO(output.read(-1)) - output.close() - unpickler = cPickle.Unpickler(msgio) - # cPickle mechanism to import instances (pickle differs here) - unpickler.find_global = checkfunction - res = unpickler.load() - if exception: - raise TrytonServerError(*(list(res[0]) + [res[1]])) - else: - return res[0] diff --git a/tryton/rpc.py b/tryton/rpc.py index 4597d1f307..ae69b4e60a 100644 --- a/tryton/rpc.py +++ b/tryton/rpc.py @@ -1,57 +1,43 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. import itertools -import pysocket import logging import socket import os from threading import Semaphore +from functools import partial +from tryton.jsonrpc import ServerProxy, Fault from tryton.fingerprints import Fingerprints from tryton.config import get_config_dir from tryton.ipc import Server as IPCServer from tryton.exceptions import TrytonServerError -_SOCK = None +CONNECTION = None _USER = None _USERNAME = '' _SESSION = '' +_HOST = '' +_PORT = None _DATABASE = '' CONTEXT = {} _VIEW_CACHE = {} TIMEZONE = 'utc' -SECURE = False _SEMAPHORE = Semaphore() _CA_CERTS = os.path.join(get_config_dir(), 'ca_certs') +if not os.path.isfile(_CA_CERTS): + _CA_CERTS = None +_FINGERPRINTS = Fingerprints() + +ServerProxy = partial(ServerProxy, fingerprints=_FINGERPRINTS, ca_certs=_CA_CERTS) def db_list(host, port): - global _SOCK, SECURE - _SEMAPHORE.acquire() try: - try: - if _SOCK and (_SOCK.hostname != host or _SOCK.port != port): - _SOCK.disconnect() - if _SOCK is None: - _SOCK = pysocket.PySocket(fingerprints=Fingerprints(), - ca_certs=_CA_CERTS) - if not _SOCK.connected: - _SOCK.connect(host, port) - args = (None, None, None, 'common', 'db', 'list') - logging.getLogger('rpc.request').info(repr(args)) - try: - _SOCK.send(args) - except socket.error, exception: - if exception.args[0] == 32: - _SOCK.reconnect() - _SOCK.send(args) - else: - raise - res = _SOCK.receive() - SECURE = _SOCK.ssl - finally: - _SEMAPHORE.release() - logging.getLogger('rpc.result').debug(repr(res)) - return res - except TrytonServerError, exception: + connection = ServerProxy(host, port) + logging.getLogger('rpc.request').info('common.db.list(None, None)') + result = connection.common.db.list(None, None) + logging.getLogger('rpc.result').debug(repr(result)) + return result + except Fault, exception: if exception.args[0] == 'AccessDenied': raise else: @@ -59,137 +45,90 @@ def db_list(host, port): return None def db_exec(host, port, method, *args): - global _SOCK, SECURE - _SEMAPHORE.acquire() - try: - if _SOCK and (_SOCK.hostname != host or _SOCK.port != port): - _SOCK.disconnect() - if _SOCK is None: - _SOCK= pysocket.PySocket(fingerprints=Fingerprints(), - ca_certs=_CA_CERTS) - if not _SOCK.connected: - _SOCK.connect(host, port) - args = (None, None, None, 'common', 'db', method) + args - logging.getLogger('rpc.request').info(repr(args)) - try: - _SOCK.send(args) - except socket.error, exception: - if exception.args[0] == 32: - _SOCK.reconnect() - _SOCK.send(args) - else: - raise - res = _SOCK.receive() - SECURE = _SOCK.ssl - finally: - _SEMAPHORE.release() - logging.getLogger('rpc.result').debug(repr(res)) - return res + connection = ServerProxy(host, port) + logging.getLogger('rpc.request').info('common.db.%s(None, None, %s)' % + (method, args)) + result = getattr(connection.common.db, method)(None, None, *args) + logging.getLogger('rpc.result').debug(repr(result)) + return result def server_version(host, port): - global _SOCK, SECURE - _SEMAPHORE.acquire() try: - try: - if _SOCK and (_SOCK.hostname != host or _SOCK.port != port): - _SOCK.disconnect() - if _SOCK is None: - _SOCK = pysocket.PySocket(fingerprints=Fingerprints(), - ca_certs=_CA_CERTS) - if not _SOCK.connected: - _SOCK.connect(host, port) - args = (None, None, None, 'common', None, 'version') - logging.getLogger('rpc.request').info(repr(args)) - try: - _SOCK.send(args) - except socket.error, exception: - if exception.args[0] == 32: - _SOCK.reconnect() - _SOCK.send(args) - else: - raise - res = _SOCK.receive() - SECURE = _SOCK.ssl - finally: - _SEMAPHORE.release() - logging.getLogger('rpc.result').debug(repr(res)) - return res - except (TrytonServerError, socket.error): + connection = ServerProxy(host, port) + logging.getLogger('rpc.request').info( + 'common.server.version(None, None)') + result = connection.common.server.version(None, None) + logging.getLogger('rpc.result').debug(repr(result)) + return result + except (Fault, socket.error): logging.getLogger('rpc.result').debug(repr(None)) return None def login(username, password, host, port, database): - global _SOCK, _USER, _USERNAME, _SESSION, _DATABASE, _VIEW_CACHE, SECURE + global CONNECTION, _USER, _USERNAME, _SESSION, _HOST, _PORT, _DATABASE, _VIEW_CACHE _VIEW_CACHE = {} - SECURE = False try: _SEMAPHORE.acquire() try: - if _SOCK and (_SOCK.hostname != host or _SOCK.port != port): - _SOCK.disconnect() - if _SOCK is None: - _SOCK = pysocket.PySocket(fingerprints=Fingerprints(), - ca_certs=_CA_CERTS) - if not _SOCK.connected: - _SOCK.connect(host, port) - args = (database, username, password, 'common', 'db', 'login') - logging.getLogger('rpc.request').info(repr(args)) - _SOCK.send(args) - res = _SOCK.receive() - logging.getLogger('rpc.result').debug(repr(res)) + connection = ServerProxy(host, port, database) + if str(connection) != str(CONNECTION): + if CONNECTION: + CONNECTION.close() + CONNECTION = connection + else: + connection = CONNECTION + logging.getLogger('rpc.request').info('common.db.login(%s, %s)' % + (username, 'x' * 10)) + result = connection.common.db.login(username, password) + logging.getLogger('rpc.result').debug(repr(result)) finally: _SEMAPHORE.release() - except (socket.error, RuntimeError): - try: - _SOCK.reconnect() - except (socket.error, RuntimeError): - pass + except socket.error: _USER = None _SESSION = '' return -1 - if not res: + if not result: _USER = None _SESSION = '' return -2 - _USER = res[0] + _USER = result[0] _USERNAME = username - _SESSION = res[1] + _SESSION = result[1] + _HOST = host + _PORT = port _DATABASE = database - SECURE = _SOCK.ssl IPCServer(host, port, database).run() return 1 def logout(): - global _SOCK, _USER, _USERNAME, _SESSION, _DATABASE, _VIEW_CACHE, SECURE + global CONNECTION, _USER, _USERNAME, _SESSION, _HOST, _PORT, _DATABASE, _VIEW_CACHE if IPCServer.instance: IPCServer.instance.stop() - if _SOCK and _USER: + if CONNECTION is not None: _SEMAPHORE.acquire() try: - args = (_DATABASE, _USER, _SESSION, 'common', 'db', 'logout') - logging.getLogger('rpc.request').info(repr(args)) - _SOCK.sock.settimeout(pysocket.CONNECT_TIMEOUT) - _SOCK.send(args) - res = _SOCK.receive() - logging.getLogger('rpc.result').debug(repr(res)) - except socket.error: + logging.getLogger('rpc.request').info('common.db.logout(%s, %s)' % + (_USER, _SESSION)) + CONNECTION.common.db.logout(_USER, _SESSION) + except (Fault, socket.error): pass finally: _SEMAPHORE.release() - _SOCK.disconnect() - _SOCK = None + CONNECTION.close() + CONNECTION = None _USER = None _USERNAME = '' _SESSION = '' + _HOST = '' + _PORT = None _DATABASE = '' _VIEW_CACHE = {} - SECURE = False def context_reload(): - global CONTEXT, TIMEZONE + global CONTEXT, TIMEZONE, _HOST, _PORT try: context = execute('model', 'res.user', 'get_preferences', True, {}) - except TrytonServerError: + except Fault: return CONTEXT = {} for i in context: @@ -197,15 +136,15 @@ def context_reload(): CONTEXT[i] = value if i == 'timezone': try: - TIMEZONE = execute('common', None, 'timezone_get') - except TrytonServerError: + connection = ServerProxy(_HOST, _PORT) + TIMEZONE = connection.common.server.timezone_get(None, None) + except Fault: pass def _execute(blocking, *args): - global _SOCK, _DATABASE, _USER, _SESSION - if not _SOCK or not _SOCK.connected: + global CONNECTION, _USER, _SESSION + if CONNECTION is None: raise TrytonServerError('NotLogged') - logging.getLogger('rpc.request').info(repr((args))) key = False if args[2] == 'fields_view_get': args, ctx = args[:-1], args[-1] @@ -223,17 +162,10 @@ def _execute(blocking, *args): if not res: return try: - try: - _SOCK.send((_DATABASE, _USER, _SESSION) + args) - result = _SOCK.receive() - except (socket.error, RuntimeError): - try: - _SOCK.reconnect() - _SOCK.send((_DATABASE, _USER, _SESSION) + args) - result = _SOCK.receive() - except (socket.error, RuntimeError): - _SOCK.reconnect() - raise + name = '.'.join(args[:3]) + args = (_USER, _SESSION) + args[3:] + logging.getLogger('rpc.request').info('%s%s' % (name, args)) + result = getattr(CONNECTION, name)(*args) finally: _SEMAPHORE.release() if key: