from tornado.web import *
import json
import hashlib
import hmac
from invocation import *
from exceptions import *
from tornado.options import define, options
import base64
from tornado.httputil import parse_multipart_form_data
from tornado.ioloop import IOLoop
from tornado.gen import coroutine, Return, engine
from tornado.concurrent import return_future, Future
from uuid import uuid4
import logging
define("allow_origin", default="*", help="This is the value for the Access-Control-Allow-Origin header (default *)")
define("method_select", default="both", metavar="both|url|parameter", help="Selects whether methods can be specified via URL, parameter in the message body or both (default both)")
define("bson_enabled", default=False, help="Allows requests to use BSON with content-type application/bson")
define("msgpack_enabled", default=False, help="Allows requests to use MessagePack with content-type application/msgpack")
define("hmac_enabled", default=True, help="Uses the x-toto-hmac header to verify authenticated requests.")
class BatchHandlerProxy(object):
'''A proxy to a handler, this class intercepts calls to ``handler.respond()`` in order to match the
response to the proper batch ``request_key``. If a method is invoked as part of a batch request,
an instance of ``BatchHandlerProxy`` will be passed instead of a ``TotoHandler``. Though this
replacement should be transparent to the method invocation, you may access the underlying handler
with ``proxy.handler``.
'''
_non_proxy_keys = {'handler', 'request_key', 'async', 'transaction_id'}
def __init__(self, handler, request_key):
self.handler = handler
self.request_key = request_key
self.transaction_id = uuid4()
def __getattr__(self, attr):
return getattr(self.handler, attr)
def __setattr__(self, attr, value):
if attr in self._non_proxy_keys:
self.__dict__[attr] = value
else:
setattr(self.handler, attr, value)
def respond(self, result=None, error=None, allow_async=True):
'''Sets the response for the corresponding batch ``request_key``. When all requests have been processed,
the combined response will passed to the underlying handler's ``respond()``.
The ``allow_async`` parameter is for internal use only and is not intended to be supplied manually.
'''
#if the handler is processing an async method, schedule the response on the main runloop
if self.async and allow_async:
IOLoop.instance().add_callback(lambda: self.respond(result, error, False))
return
self._after_invoke(self.transaction_id)
self.handler.batch_results[self.request_key] = error is not None and {'error': isinstance(error, dict) and error or self.handler.error_info(error)} or {'result': result}
if len(self.handler.batch_results) == len(self.handler.request_keys):
self.handler.respond(batch_results=self.handler.batch_results, allow_async=False)
class TotoHandler(RequestHandler):
'''The handler is responsible for processing all requests to the server. An instance
will be initialized for each incoming request and will handle authentication, session
management and method delegation for you.
You can set the module to use for method delegation via the ``method_module`` parameter.
Methods are modules that contain an invoke function::
def invoke(handler, parameters)
The request handler will be passed as the first parameter to the invoke function and
provides access to the server's database connection, the current session and other
useful request properties. Request parameters will be passed as the second argument
to the invoke function. Any return values from ``invoke()`` functions should be
JSON serializable.
Toto methods are generally invoked via a POST request to the server with a JSON
serialized object as the body. The body should contain two properties:
1. method - The name of the method to invoke.
2. parameters - Any parameters to pass to the Toto function.
For example::
{"method": "account.create", "parameters": {"user_id": "test", "password": "testpassword"}}
Will call method_module.account.create.invoke(handler, {'user_id': 'test', 'password': 'testpassword'})
An ``invoke()`` function can be decorated with ``@tornado.gen.coroutine`` and be run as a Tornado coroutine.
Alternatively, an ``invoke(handler, parameters)`` function may be decorated with ``@toto.invocation.asynchronous``.
If a function decorated in this manner does not return a value, the connection well remain open until
``handler.respond(result, error)`` is called where ``error`` is an ``Exception`` or ``None`` and ``result``
is a normal ``invoke()`` return value or ``None``.
There are client libraries for iOS and Javascript that will make using Toto much easier. They are
available at https://github.com/JeremyOT/TotoClient-iOS and https://github.com/JeremyOT/TotoClient-JS
respectively.
'''
SUPPORTED_METHODS = {"POST", "OPTIONS", "GET", "HEAD"}
ACCESS_CONTROL_ALLOW_ORIGIN = options.allow_origin
def initialize(self, db_connection):
self.db_connection = db_connection
self.db = self.db_connection.db
self.bson = options.bson_enabled and __import__('bson').BSON
self.msgpack = options.msgpack_enabled and __import__('msgpack')
self.response_type = 'application/json'
self.body = None
self.registered_event_handlers = []
self.__active_methods = []
self.headers_only = False
self.async = False
self.transaction_id = uuid4()
@classmethod
def configure(cls):
"""Runtime method configuration.
"""
#Method configuration
if options.event_mode != 'off':
from toto.events import EventManager
cls.event_manager = EventManager
if options.method_select == 'url':
def get_method_path(self, path, body):
if path:
return '.'.join(path.split('/'))
else:
raise TotoException(ERROR_MISSING_METHOD, "Missing method.")
cls.__get_method_path = get_method_path
elif options.method_select == 'parameter':
def get_method_path(self, path, body):
if body and 'method' in body:
logging.info(body['method'])
return body['method']
else:
raise TotoException(ERROR_MISSING_METHOD, "Missing method.")
cls.__get_method_path = get_method_path
if options.use_cookies:
import math
set_cookie = options.secure_cookies and cls.set_secure_cookie or cls.set_cookie
get_cookie = options.secure_cookies and cls.get_secure_cookie or cls.get_cookie
def create_session(self, user_id=None, password=None, verify_password=True):
self.session = self.db_connection.create_session(user_id, password, verify_password=verify_password)
set_cookie(self, name='toto-session-id', value=self.session.session_id, expires_days=math.ceil(self.session.expires / (24.0 * 60.0 * 60.0)), domain=options.cookie_domain)
return self.session
cls.create_session = create_session
def retrieve_session(self, session_id=None):
if not self.session or (session_id and self.session.session_id != session_id):
headers = self.request.headers
if not session_id:
session_id = 'x-toto-session-id' in headers and headers['x-toto-session-id'] or get_cookie(self, 'toto-session-id')
if session_id:
self.session = self.db_connection.retrieve_session(session_id, options.hmac_enabled and headers.get('x-toto-hmac'), options.hmac_enabled and 'x-toto-hmac' in headers and self.request.body or None)
if self.session:
set_cookie(self, name='toto-session-id', value=self.session.session_id, expires_days=math.ceil(self.session.expires / (24.0 * 60.0 * 60.0)), domain=options.cookie_domain)
return self.session
cls.retrieve_session = retrieve_session
if options.debug:
import traceback
def error_info(self, e):
if not isinstance(e, TotoException):