__init__.py 10.4 KB
import decimal
import json
import logging
from datetime import datetime

import colander
from icecream import ic
from pyramid.httpexceptions import HTTPForbidden
from pyramid.httpexceptions import HTTPNotFound
from pyramid.renderers import null_renderer, render, JSON
from pyramid.response import Response
from pyramid.security import NO_PERMISSION_REQUIRED, remember
from pyramid_rpc.jsonrpc import (
    JsonRpcError, JsonRpcMethodNotFound, JsonRpcParamsInvalid,
    JsonRpcInternalError, make_error_response, MethodPredicate,
    BatchedRequestPredicate, jsonrpc_renderer, add_jsonrpc_method,
    DEFAULT_RENDERER, batched_request_view, Endpoint,
    JsonRpcRequestInvalid, parse_request_GET, parse_request_POST)
from pyramid_rpc.mapper import ViewMapperArgsInvalid, MapplyViewMapper

from opensipkd.base.tools.api import auth_from_rpc

log = logging.getLogger(__name__)


class JsonRpcRequestForbidden(JsonRpcError):
    code = -32604
    message = 'request forbidden'


class JsonRpcInvalidLogin(JsonRpcError):
    code = -32605
    message = "Invalid User/Password"


#
# class EndpointPredicate(BaseEndpointPredicate):
#     def __call__(self, info, request):
#         if self.val:
#             # find the endpoint info
#             key = info['route'].name
#             endpoint = request.registry.jsonrpc_endpoints[key]
#
#             # potentially setup either rpc v1 or v2 from the parsed body
#             setup_request(endpoint, request)
#
#             # update request with endpoint information
#             request.rpc_endpoint = endpoint
#
#             # Always return True so that even if it isn't a valid RPC it
#             # will fall through to the notfound_view which will still
#             # return a valid JSON-RPC response.
#             return True


# def setup_request(endpoint, request):
#     """ Parse a JSON-RPC request body."""
#     if request.method == 'GET':
#         parse_request_GET(request)
#     elif request.method == 'POST':
#         parse_request_POST(request)
#     else:
#         log.debug('unsupported request method "%s"', request.method)
#         raise JsonRpcRequestInvalid
#
#     if hasattr(request, 'batched_rpc_requests'):
#         log.debug('handling batched rpc request')
#         # the checks below will look at the subrequests
#         return
#
#     if request.rpc_version != '2.0':
#         log.debug('id:%s invalid rpc version %s',
#                   request.rpc_id, request.rpc_version)
#         raise JsonRpcRequestInvalid
#
#     if request.rpc_method is None:
#         log.debug('id:%s invalid rpc method', request.rpc_id)
#         raise JsonRpcRequestInvalid

# env = request.environ
# if 'HTTP_TOKEN' in env:
#     try:
#         user_device = token_auth(request)
#         user = user_device.user
#         headers = remember(request, user.id)
#         request.headers["Cookie"] = dict(headers)["Set-Cookie"]
#         request.headers["token"]=user_device.token
#         log.debug(request.headers["Cookie"])
#     except JsonRpcInvalidLoginError as e:
#         raise JsonRpcInvalidLogin
#
# elif ('HTTP_USERID' in env and 'HTTP_SIGNATURE' in env and
#       'HTTP_KEY' in env):
#     try:
#         user = rpc_auth(request)
#         headers = remember(request, user.id)
#         request.headers["Cookie"] = dict(headers)["Set-Cookie"]
#         log.debug(request.headers["Cookie"])
#     except JsonRpcInvalidLoginError as e:
#         raise JsonRpcInvalidLogin

# log.debug('handling id:%s method:%s',
#           request.rpc_id, request.rpc_method)

def make_error_response(request, error, id=None):
    """ Marshal a Python Exception into a ``Response`` object with a
    body that is a JSON string suitable for use as a JSON-RPC response
    with a content-type of ``application/json`` and return the response.

    """
    # we may need to render a parse error, at which point we don't know
    # much about the request
    renderer = getattr(request, 'rpc_renderer', DEFAULT_RENDERER)
    out = {
        'jsonrpc': '2.0',
        'id': id,
        'error': error.as_dict(),
    }
    ic(out)
    body = render(renderer, out, request=request).encode('utf-8')

    response = Response(body, charset='utf-8')
    response.content_type = 'application/json'
    return response

def exception_view(exc, request):
    rpc_id = getattr(request, 'rpc_id', None)
    if isinstance(exc, JsonRpcError):
        fault = exc
        log.debug('json-rpc error rpc_id:%s "%s"',
                  rpc_id, exc.message)
    elif isinstance(exc, HTTPNotFound):
        fault = JsonRpcMethodNotFound()
        log.debug('json-rpc method not found rpc_id:%s "%s"',
                  rpc_id, request.rpc_method)
    elif isinstance(exc, HTTPForbidden):
        fault = JsonRpcRequestForbidden()
        log.debug('json-rpc method forbidden rpc_id:%s "%s"',
                  rpc_id, request.rpc_method)
    elif isinstance(exc, ViewMapperArgsInvalid):
        fault = JsonRpcParamsInvalid()
        log.debug('json-rpc invalid method params')
    else:
        fault = JsonRpcInternalError()
        log.exception('json-rpc exception rpc_id:%s "%s"', rpc_id, exc)

    return make_error_response(request, fault, rpc_id)


def add_jsonrpc_endpoint(config, name, *args, **kw):
    """Add an endpoint for handling JSON-RPC.

    ``name``

        The name of the endpoint.

    ``default_mapper``

        A default view mapper that will be passed as the ``mapper``
        argument to each of the endpoint's methods.

    ``default_renderer``

        A default renderer that will be passed as the ``renderer``
        argument to each of the endpoint's methods. This should be the
        string name of the renderer, registered via
        :meth:`pyramid.config.Configurator.add_renderer`.

    A JSON-RPC method also accepts all of the arguments supplied to
    :meth:`pyramid.config.Configurator.add_route`.

    """
    default_mapper = kw.pop('default_mapper', MapplyViewMapper)
    default_renderer = kw.pop('default_renderer', DEFAULT_RENDERER)

    endpoint = Endpoint(
        name,
        default_mapper=default_mapper,
        default_renderer=default_renderer,
    )

    config.registry.jsonrpc_endpoints[name] = endpoint

    kw['jsonrpc_endpoint'] = True
    config.add_route(name, *args, **kw)

    kw = {}
    kw['jsonrpc_batched'] = True
    kw['renderer'] = null_renderer
    config.add_view(batched_request_view, route_name=name,
                    permission=NO_PERMISSION_REQUIRED, **kw)
    config.add_view(exception_view, route_name=name, context=Exception,
                    permission=NO_PERMISSION_REQUIRED)


def setup_request(endpoint, request):
    """ Parse a JSON-RPC request body."""
    print(request.__dict__)
    print(request.environ.get("ACCESS_CONTROL_ALLOW_ORIGIN"))
    if request.method == 'GET':
        parse_request_GET(request)
    elif request.method == 'POST':
        parse_request_POST(request)
    else:
        log.debug('unsupported request method "%s"', request.method)
        raise JsonRpcRequestInvalid

    if hasattr(request, 'batched_rpc_requests'):
        log.debug('handling batched rpc request')
        # the checks below will look at the subrequests
        return

    if request.rpc_version != '2.0':
        log.debug('id:%s invalid rpc version %s',
                  request.rpc_id, request.rpc_version)
        raise JsonRpcRequestInvalid

    if request.rpc_method is None:
        log.debug('id:%s invalid rpc method', request.rpc_id)
        raise JsonRpcRequestInvalid

    log.debug('handling id:%s method:%s',
              request.rpc_id, request.rpc_method)
    env = request.environ
    if 'HTTP_USERID' in env or 'HTTP_SIGNATURE' in env or 'HTTP_KEY' in env:
        user = auth_from_rpc(request)
        if user:
            headers = remember(request, user.id)
            for k, v in headers:
                if k == "Set-Cookie":
                    req_headers = [("Cookie", v)]
                    request.headers.update(req_headers)
                    log.debug('handling request headers: %s', req_headers)


class EndpointPredicate(object):
    def __init__(self, val, config):
        self.val = val

    def text(self):
        return 'jsonrpc endpoint = %s' % self.val

    phash = text

    def __call__(self, info, request):
        if self.val:
            # find the endpoint info
            key = info['route'].name
            endpoint = request.registry.jsonrpc_endpoints[key]

            # potentially setup either rpc v1 or v2 from the parsed body
            setup_request(endpoint, request)

            # update request with endpoint information
            request.rpc_endpoint = endpoint

            # Always return True so that even if it isn't a valid RPC it
            # will fall through to the notfound_view which will still
            # return a valid JSON-RPC response.
            return True

def json_rpc():
    json_r = JSON()
    json_r.add_adapter(datetime.datetime, lambda v, request: v.isoformat())
    json_r.add_adapter(datetime.date, lambda v, request: v.isoformat())
    json_r.add_adapter(decimal.Decimal, lambda v, request: str(v))
    json_r.add_adapter(colander.null, lambda v, request: None)
    return json_r

def includeme(config):
    """ Set up standard configurator registrations.  Use via:

    .. code-block:: python

       config = Configurator()
       config.include('pyramid_rpc.jsonrpc')

    Once this function has been invoked, two new directives will be
    available on the configurator:

    - ``add_jsonrpc_endpoint``: Add an endpoint for handling JSON-RPC.

    - ``add_jsonrpc_method``: Add a method to a JSON-RPC endpoint.

    """
    if not hasattr(config.registry, 'jsonrpc_endpoints'):
        config.registry.jsonrpc_endpoints = {}

    config.add_view_predicate('jsonrpc_method', MethodPredicate)
    config.add_view_predicate('jsonrpc_batched', BatchedRequestPredicate)
    config.add_route_predicate('jsonrpc_endpoint', EndpointPredicate)

    config.add_renderer(DEFAULT_RENDERER, jsonrpc_renderer)
    config.add_directive('add_jsonrpc_endpoint', add_jsonrpc_endpoint)
    config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
    config.add_view(exception_view, context=JsonRpcError,
                    permission=NO_PERMISSION_REQUIRED)