Files
tryton/trytond/protocols/jsonrpc.py
T

345 lines
12 KiB
Python

#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 trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.config import CONFIG
from trytond.protocols.datatype import Float
import SimpleXMLRPCServer
import SimpleHTTPServer
import SocketServer
import threading
import traceback
import socket
import sys
import os
import gzip
import StringIO
try:
import fcntl
except ImportError:
fcntl = None
import posixpath
import urllib
import datetime
from decimal import Decimal
if sys.version_info < (2, 6):
import simplejson as json
else:
import json
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'])
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 float(obj)
return super(JSONEncoder, self).default(obj)
class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
"""Mix-in class that dispatches JSON-RPC requests.
This class is used to register JSON-RPC method handlers
and then to dispatch them. There should never be any
reason to instantiate this class directly.
"""
def _marshaled_dispatch(self, data, dispatch_method=None):
"""Dispatches an JSON-RPC method from marshalled (JSON) data.
JSON-RPC methods are dispatched from the marshalled (JSON) data
using the _dispatch method and the result is returned as
marshalled data. For backwards compatibility, a dispatch
function can be provided as an argument (see comment in
SimpleJSONRPCRequestHandler.do_POST) but overriding the
existing method through subclassing is the prefered means
of changing method dispatch behavior.
"""
rawreq = json.loads(data, object_hook=object_hook, parse_float=Float)
req_id = rawreq.get('id', 0)
method = rawreq['method']
params = rawreq.get('params', [])
response = {'id': req_id}
try:
#generate response
if dispatch_method is not None:
response['result'] = dispatch_method(method, params)
else:
response['result'] = self._dispatch(method, params)
except:
tb_s = ''
for line in traceback.format_exception(*sys.exc_info()):
try:
line = line.encode('utf-8', 'ignore')
except:
continue
tb_s += line
for path in sys.path:
tb_s = tb_s.replace(path, '')
if CONFIG['debug_mode']:
import pdb
traceb = sys.exc_info()[2]
pdb.post_mortem(traceb)
# report exception back to server
response['error'] = "%s:\n%s" % (sys.exc_value, tb_s)
return json.dumps(response, cls=JSONEncoder)
class GenericJSONRPCRequestHandler:
def _dispatch(self, method, params):
host, port = self.client_address[:2]
database_name = self.path[1:]
if database_name.startswith('sao/'):
database_name = database_name[4:]
method_list = method.split('.')
object_type = method_list[0]
object_name = '.'.join(method_list[1:-1])
method = method_list[-1]
args = (host, port, 'JSON-RPC', database_name, params[0], params[1],
object_type, object_name, method) + tuple(params[2:])
res = dispatch(*args)
return res
class SimpleJSONRPCRequestHandler(GenericJSONRPCRequestHandler,
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler,
SimpleHTTPServer.SimpleHTTPRequestHandler):
"""Simple JSON-RPC request handler class.
Handles all HTTP POST requests and attempts to decode them as
JSON-RPC requests.
"""
rpc_paths = None
encode_threshold = 1400 # common MTU
# Copy from SimpleXMLRPCServer.py with gzip encoding added
def do_POST(self):
"""Handles the HTTP POST request.
Attempts to interpret all HTTP POST requests as JSON-RPC calls,
which are forwarded to the server's _dispatch method for handling.
"""
# Check that the path is legal
if not self.is_rpc_path_valid():
self.report_404()
return
try:
# Get arguments by reading body of request.
# We read this in chunks to avoid straining
# socket.read(); around the 10 or 15Mb mark, some platforms
# begin to have problems (bug #792570).
max_chunk_size = 10*1024*1024
size_remaining = int(self.headers["content-length"])
L = []
while size_remaining:
chunk_size = min(size_remaining, max_chunk_size)
L.append(self.rfile.read(chunk_size))
size_remaining -= len(L[-1])
data = ''.join(L)
# In previous versions of SimpleXMLRPCServer, _dispatch
# could be overridden in this class, instead of in
# SimpleXMLRPCDispatcher. To maintain backwards compatibility,
# check to see if a subclass implements _dispatch and dispatch
# using that method if present.
response = self.server._marshaled_dispatch(
data, getattr(self, '_dispatch', None)
)
except: # This should only happen if the module is buggy
# internal error, report as HTTP server error
self.send_response(500)
self.end_headers()
else:
# got a valid JSON RPC response
self.send_response(200)
self.send_header("Content-type", "application/json-rpc")
# Handle gzip encoding
if 'gzip' in self.headers.get('Accept-Encoding', '').split(',') \
and len(response) > self.encode_threshold:
buffer = StringIO.StringIO()
output = gzip.GzipFile(mode='wb', fileobj=buffer)
output.write(response)
output.close()
buffer.seek(0)
response = buffer.getvalue()
self.send_header('Content-Encoding', 'gzip')
self.send_header("Content-length", str(len(response)))
self.end_headers()
self.wfile.write(response)
# shut down the connection
self.wfile.flush()
self.connection.shutdown(1)
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.
Components that mean special things to the local file system
(e.g. drive or directory names) are ignored. (XXX They should
probably be diagnosed.)
"""
# abandon query parameters
path = path.split('?', 1)[0]
path = path.split('#', 1)[0]
path = posixpath.normpath(urllib.unquote(path))
words = path.split('/')
words = filter(None, words)
path = CONFIG['jsondata_path']
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in (os.curdir, os.pardir): continue
path = os.path.join(path, word)
return path
class SecureJSONRPCRequestHandler(SimpleJSONRPCRequestHandler):
def setup(self):
self.connection = SSLSocket(self.request)
self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
class SimpleJSONRPCServer(SocketServer.TCPServer,
SimpleJSONRPCDispatcher):
"""Simple JSON-RPC server.
Simple JSON-RPC server that allows functions and a single instance
to be installed to handle requests. The default implementation
attempts to dispatch JSON-RPC calls to the functions or instance
installed in the server. Override the _dispatch method inhereted
from SimpleJSONRPCDispatcher to change this behavior.
"""
allow_reuse_address = True
# Warning: this is for debugging purposes only! Never set this to True in
# production code, as will be sending out sensitive information (exception
# and stack trace details) when exceptions are raised inside
# SimpleJSONRPCRequestHandler.do_POST
_send_traceback_header = False
def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
logRequests=True, allow_none=False, encoding=None,
bind_and_activate=True):
self.logRequests = logRequests
SimpleJSONRPCDispatcher.__init__(self, allow_none, encoding)
try:
SocketServer.TCPServer.__init__(self, addr, requestHandler,
bind_and_activate)
except TypeError:
SocketServer.TCPServer.__init__(self, addr, requestHandler)
# [Bug #1222790] If possible, set close-on-exec flag; if a
# method spawns a subprocess, the subprocess shouldn't have
# the listening socket open.
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
flags |= fcntl.FD_CLOEXEC
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
class SimpleThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
SimpleJSONRPCServer):
timeout = 1
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
SimpleJSONRPCServer.server_bind(self)
class SimpleThreadedJSONRPCServer6(SimpleThreadedJSONRPCServer):
address_family = socket.AF_INET6
class SecureThreadedJSONRPCServer(SimpleThreadedJSONRPCServer):
def __init__(self, server_address, HandlerClass, logRequests=1):
SimpleThreadedJSONRPCServer.__init__(self, server_address, HandlerClass,
logRequests)
self.socket = SSLSocket(socket.socket(self.address_family,
self.socket_type))
self.server_bind()
self.server_activate()
class SecureThreadedJSONRPCServer6(SecureThreadedJSONRPCServer):
address_family = socket.AF_INET6
class JSONRPCDaemon(threading.Thread):
def __init__(self, interface, port, secure=False):
threading.Thread.__init__(self)
self.secure = secure
self.running = False
ipv6 = False
if socket.has_ipv6:
try:
socket.getaddrinfo(interface or None, port, socket.AF_INET6)
ipv6 = True
except:
pass
if secure:
handler_class = SecureJSONRPCRequestHandler
server_class = SecureThreadedJSONRPCServer
if ipv6:
server_class = SecureThreadedJSONRPCServer6
else:
handler_class = SimpleJSONRPCRequestHandler
server_class = SimpleThreadedJSONRPCServer
if ipv6:
server_class = SimpleThreadedJSONRPCServer6
self.server = server_class((interface, port), handler_class, 0)
def stop(self):
self.running = False
if os.name != 'nt':
if hasattr(socket, 'SHUT_RDWR'):
self.server.socket.shutdown(socket.SHUT_RDWR)
else:
self.server.socket.shutdown(2)
self.server.socket.close()
def run(self):
self.running = True
while self.running:
self.server.handle_request()
return True