Esempio n. 1
0
File: mole.py Progetto: ruter/Mole
class Mole(object):
    """ WSGI application or Handler """
    def __init__(self, catchall=True, autojson=True, config=None):
        """ Create a new mole instance.
            You usually don't do that. Use `mole.app.push()` instead.
        """
        self.routes = []  # List of installed routes including metadata.
        self.callbacks = {}  # Cache for wrapped callbacks.
        self.router = Router()  # Maps to self.routes indices.

        self.mounts = {}
        self.error_handler = {}
        self.catchall = catchall
        self.config = config or {}
        self.serve = True
        self.castfilter = []
        if autojson and json_dumps:
            self.add_filter(dict, dict2json)
        self.hooks = {'before_request': [], 'after_request': []}

    def optimize(self, *a, **ka):
        utils.depr("Mole.optimize() is obsolete.")

    def mount(self, app, script_path):
        ''' Mount a Mole application to a specific URL prefix '''
        if not isinstance(app, Mole):
            raise TypeError('Only Mole instances are supported for now.')
        script_path = '/'.join(filter(None, script_path.split('/')))
        path_depth = script_path.count('/') + 1
        if not script_path:
            raise TypeError('Empty script_path. Perhaps you want a merge()?')
        for other in self.mounts:
            if other.startswith(script_path):
                raise TypeError('Conflict with existing mount: %s' % other)

        @self.route('/%s/:#.*#' % script_path, method="ANY")
        def mountpoint():
            request.path_shift(path_depth)
            return app.handle(request.environ)

        self.mounts[script_path] = app

    def add_filter(self, ftype, func):
        ''' Register a new output filter. Whenever mole hits a handler output
            matching `ftype`, `func` is applied to it. '''
        if not isinstance(ftype, type):
            raise TypeError("Expected type object, got %s" % type(ftype))
        self.castfilter = [(t, f) for (t, f) in self.castfilter if t != ftype]
        self.castfilter.append((ftype, func))
        self.castfilter.sort()

    def match_url(self, path, method='GET'):
        return self.match({'PATH_INFO': path, 'REQUEST_METHOD': method})

    def match(self, environ):
        """ Return a (callback, url-args) tuple or raise HTTPError. """
        target, args = self.router.match(environ)
        try:
            return self.callbacks[target], args
        except KeyError:
            callback, decorators = self.routes[target]
            wrapped = callback
            for wrapper in decorators[::-1]:
                wrapped = wrapper(wrapped)
            #for plugin in self.plugins or []:
            #    wrapped = plugin.apply(wrapped, rule)
            functools.update_wrapper(wrapped, callback)
            self.callbacks[target] = wrapped
            return wrapped, args

    def get_url(self, routename, **kargs):
        """ Return a string that matches a named route """
        scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
        location = self.router.build(routename, **kargs).lstrip('/')
        return urljoin(urljoin('/', scriptname), location)

    def route(self,
              path=None,
              method='GET',
              no_hooks=False,
              decorate=None,
              template=None,
              template_opts={},
              callback=None,
              name=None,
              static=False):
        """ Decorator: Bind a callback function to a request path.

            :param path: The request path or a list of paths to listen to. See 
              :class:`Router` for syntax details. If no path is specified, it
              is automatically generated from the callback signature. See
              :func:`yieldroutes` for details.
            :param method: The HTTP method (POST, GET, ...) or a list of
              methods to listen to. (default: GET)
            :param decorate: A decorator or a list of decorators. These are
              applied to the callback in reverse order (on demand only).
            :param no_hooks: If true, application hooks are not triggered
              by this route. (default: False)
            :param template: The template to use for this callback.
              (default: no template)
            :param template_opts: A dict with additional template parameters.
            :param name: The name for this route. (default: None)
            :param callback: If set, the route decorator is directly applied
              to the callback and the callback is returned instead. This
              equals ``Mole.route(...)(callback)``.
        """
        # @route can be used without any parameters
        if callable(path): path, callback = None, path
        # Build up the list of decorators
        decorators = makelist(decorate)
        if template: decorators.insert(0, view(template, **template_opts))
        if not no_hooks: decorators.append(self._add_hook_wrapper)

        #decorators.append(partial(self.apply_plugins, skiplist))
        def wrapper(func):
            for rule in makelist(path) or yieldroutes(func):
                for verb in makelist(method):
                    if static:
                        rule = rule.replace(':', '\\:')
                        utils.depr("Use backslash to escape ':' in routes.")
                    #TODO: Prepare this for plugins
                    self.router.add(rule, verb, len(self.routes), name=name)
                    self.routes.append((func, decorators))
            return func

        return wrapper(callback) if callback else wrapper

    def _add_hook_wrapper(self, func):
        ''' Add hooks to a callable. See #84 '''
        @functools.wraps(func)
        def wrapper(*a, **ka):
            for hook in self.hooks['before_request']:
                hook()
            response.output = func(*a, **ka)
            for hook in self.hooks['after_request']:
                hook()
            return response.output

        return wrapper

    def get(self, path=None, method='GET', **kargs):
        """ Decorator: Bind a function to a GET request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def post(self, path=None, method='POST', **kargs):
        """ Decorator: Bind a function to a POST request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def put(self, path=None, method='PUT', **kargs):
        """ Decorator: Bind a function to a PUT request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def delete(self, path=None, method='DELETE', **kargs):
        """ Decorator: Bind a function to a DELETE request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def error(self, code=500):
        """ Decorator: Register an output handler for a HTTP error code"""
        def wrapper(handler):
            self.error_handler[int(code)] = handler
            return handler

        return wrapper

    def hook(self, name):
        """ Return a decorator that adds a callback to the specified hook. """
        def wrapper(func):
            self.add_hook(name, func)
            return func

        return wrapper

    def add_hook(self, name, func):
        ''' Add a callback from a hook. '''
        if name not in self.hooks:
            raise ValueError("Unknown hook name %s" % name)
        if name in ('after_request'):
            self.hooks[name].insert(0, func)
        else:
            self.hooks[name].append(func)

    def remove_hook(self, name, func):
        ''' Remove a callback from a hook. '''
        if name not in self.hooks:
            raise ValueError("Unknown hook name %s" % name)
        self.hooks[name].remove(func)

    def handle(self, environ):
        """ Execute the handler bound to the specified url and method and return
        its output. If catchall is true, exceptions are catched and returned as
        HTTPError(500) objects. """
        if not self.serve:
            return HTTPError(503, "Server stopped")
        try:
            handler, args = self.match(environ)
            return handler(**args)
        except HTTPResponse, e:
            return e
        except Exception, e:
            import traceback
            traceback.print_exc()
            if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\
            or not self.catchall:
                raise
            return HTTPError(500, 'Unhandled exception', e, format_exc(10))
class JsonProxyRestHandler(splunk.rest.BaseRestHandler):
    def __init__(self, *args, **kwargs):
        super(JsonProxyRestHandler, self).__init__(*args, **kwargs)
        
        self.router = Router()
        
        self.router.add(Route('/search/jobs/<sid>/control', {"POST": self.job_control}, 'job_control'))
        self.router.add(Route('/search/jobs/<sid>/<data_source>', {"GET": self.job_data}, 'job_data'))
        self.router.add(Route('/search/jobs/<sid>', {"GET": self.eai, "DELETE": self.delete_job}, 'job_info'))
        self.router.add(Route('/search/jobs', {"GET": self.eai, "POST": self.create_job}, 'jobs'))
        self.router.add(Route('/search/parser', self.parse_query, 'parse_query'))
        self.router.add(Route('/search/typeahead', self.typeahead, 'typeahead'))
        self.router.add(Route('/search/tags/<name>', 
            {
                "GET": self.eai, 
                "DELETE": self.modify_or_delete_tag, 
                "POST": self.modify_or_delete_tag
            }, 
            'tag_info'
        ))
        self.router.add(Route('/properties/<file>/<stanza>', self.properties_stanza, 'properties_stanza_info'))
        self.router.add(Route('/properties/<file>/<stanza>/<key>', self.properties_stanza_key, 'properties_stanza_key'))
        self.router.add(Route('/receivers/simple', self.http_simple_input, 'http_simple_input'))
        self.router.add(Route('/auth/login', {"POST": self.auth}, 'auth'))
        self.router.add(Route('/<:.*>', self.eai, 'eai'))
        
    # UNDONE
    # This allows us to use basic auth, but it's not the ideal way to do this.
    # The problem is that we want to be able to reuse the code in splunk.rest.simpleRequest,
    # but that code does not allow us to set headers. As such, we have to create this wrapper
    # class.
    def wrap_http(self):   
        is_basicauth = self.is_basicauth()
        basicauth = self.get_authorization()
        
        class Http(httplib2.Http):            
            def request(self, *args, **kwargs):
                if is_basicauth and kwargs.has_key("headers"):
                    kwargs["headers"]["Authorization"] = basicauth
                    
                return super(Http,self).request(*args, **kwargs)
                
        return Http
    
    def extract_path(self):
        self.scrubbed_path = self.request['path'].replace("/services/json/v1", "")
        if re.match(r"^/servicesNS/[^/]*/[^/]*", self.scrubbed_path):
            self.scrubbed_path = re.sub(r"^(/servicesNS/[^/]*/[^/]*)(/.*)", r"\2", self.scrubbed_path)
        elif re.match(r"^/services/.*", self.scrubbed_path):
            self.scrubbed_path = re.sub(r"^(/services)(/.*)", r"\2", self.scrubbed_path)
            
        if self.scrubbed_path.endswith("/"):
            self.scrubbed_path = self.scrubbed_path[:-1]
            
        if self.scrubbed_path.endswith("?"):
            self.scrubbed_path = self.scrubbed_path[:-1]
    
    def extract_sessionKey(self):        
        self.sessionKey = self.request["headers"].get("authorization", "").replace("Splunk", "").strip() or None
        
    def extract_origin(self):
        if self.request["headers"].has_key(REMOTEORIGIN_HEADER):
            parsed = urlparse(self.request["headers"][REMOTEORIGIN_HEADER])
            self.remote_origin = parsed.netloc.replace(":" + str(parsed.port), "")
        else:
            self.remote_origin = self.request["remoteAddr"]
            
    def extract_allowed_domains(self):
        self.allowed_domains = None
        
        self.settings = splunk.clilib.cli_common.getConfStanza(CONF_FILE, SETTINGS_STANZA)
        self.allowed_domains = map(lambda s: s.strip(), self.settings.get(ALLOWED_DOMAINS_KEY).split(","))
        
    def is_basicauth(self):
        return self.request["headers"].get("authorization", "").startswith("Basic ")
        
    def get_authorization(self):
        return self.request["headers"].get("authorization", "")
    
    def get_origin_error(self):
        output = ODataEntity()
        output.messages.append({
            "type": "HTTP",
            "text": "Origin '%s' is not allowed. Please check json.conf" % self.remote_origin
        })
        
        return 403, self.render_odata(output)
    
    def handle(self):
        output = ODataEntity()
        status = 500
        
        try:
            self.extract_path()
            self.extract_origin()
            self.extract_sessionKey()
            self.extract_allowed_domains()
            
            # Get the appropriate handler
            handler, args, kwargs = self.router.match(self.scrubbed_path)
                
            # Check to see if we are in the list of allowed domains
            if not self.remote_origin in self.allowed_domains: 
                status, content = self.get_origin_error()        
            else:
                if isinstance(handler, dict):
                    if handler.has_key(self.method):
                        handler = handler[self.method]
                    else:
                        self.set_response(404, "")
                        return
                
                status, content = handler(*args, **kwargs)
        except splunk.RESTException, e:
            responseCode = e.statusCode
            output.messages.append({
                'type': 'HTTP',
                'text': '%s %s' % (e.statusCode, e.msg)
            })
            if hasattr(e, 'extendedMessages') and e.extendedMessages:
                for message in e.extendedMessages:
                    output.messages.append(message)
                  
            content = self.render_odata(output)
        except Exception, e:
            status = 500
            output.messages.append({
                'type': 'ERROR',
                'text': '%s' % e
            })
          
            content = self.render_odata(output)
            raise
Esempio n. 3
0
class Mole(object):
    """ WSGI application or Handler """

    def __init__(self, catchall=True, autojson=True, config=None):
        """ Create a new mole instance.
            You usually don't do that. Use `mole.app.push()` instead.
        """
        self.routes = [] # List of installed routes including metadata.
        self.callbacks = {} # Cache for wrapped callbacks.
        self.router = Router() # Maps to self.routes indices.

        self.mounts = {}
        self.error_handler = {}
        self.catchall = catchall
        self.config = config or {}
        self.serve = True
        self.castfilter = []
        if autojson and json_dumps:
            self.add_filter(dict, dict2json)
        self.hooks = {'before_request': [], 'after_request': []}

    def optimize(self, *a, **ka):
        utils.depr("Mole.optimize() is obsolete.")

    def mount(self, app, script_path):
        ''' Mount a Mole application to a specific URL prefix '''
        if not isinstance(app, Mole):
            raise TypeError('Only Mole instances are supported for now.')
        script_path = '/'.join(filter(None, script_path.split('/')))
        path_depth = script_path.count('/') + 1
        if not script_path:
            raise TypeError('Empty script_path. Perhaps you want a merge()?')
        for other in self.mounts:
            if other.startswith(script_path):
                raise TypeError('Conflict with existing mount: %s' % other)
        @self.route('/%s/:#.*#' % script_path, method="ANY")
        def mountpoint():
            request.path_shift(path_depth)
            return app.handle(request.environ)
        self.mounts[script_path] = app

    def add_filter(self, ftype, func):
        ''' Register a new output filter. Whenever mole hits a handler output
            matching `ftype`, `func` is applied to it. '''
        if not isinstance(ftype, type):
            raise TypeError("Expected type object, got %s" % type(ftype))
        self.castfilter = [(t, f) for (t, f) in self.castfilter if t != ftype]
        self.castfilter.append((ftype, func))
        self.castfilter.sort()

    def match_url(self, path, method='GET'):
        return self.match({'PATH_INFO': path, 'REQUEST_METHOD': method})
        
    def match(self, environ):
        """ Return a (callback, url-args) tuple or raise HTTPError. """
        target, args = self.router.match(environ)
        try:
            return self.callbacks[target], args
        except KeyError:
            callback, decorators = self.routes[target]
            wrapped = callback
            for wrapper in decorators[::-1]:
                wrapped = wrapper(wrapped)
            #for plugin in self.plugins or []:
            #    wrapped = plugin.apply(wrapped, rule)
            functools.update_wrapper(wrapped, callback)
            self.callbacks[target] = wrapped
            return wrapped, args

    def get_url(self, routename, **kargs):
        """ Return a string that matches a named route """
        scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
        location = self.router.build(routename, **kargs).lstrip('/')
        return urljoin(urljoin('/', scriptname), location)

    def route(self, path=None, method='GET', no_hooks=False, decorate=None,
              template=None, template_opts={}, callback=None, name=None,
              static=False):
        """ Decorator: Bind a callback function to a request path.

            :param path: The request path or a list of paths to listen to. See 
              :class:`Router` for syntax details. If no path is specified, it
              is automatically generated from the callback signature. See
              :func:`yieldroutes` for details.
            :param method: The HTTP method (POST, GET, ...) or a list of
              methods to listen to. (default: GET)
            :param decorate: A decorator or a list of decorators. These are
              applied to the callback in reverse order (on demand only).
            :param no_hooks: If true, application hooks are not triggered
              by this route. (default: False)
            :param template: The template to use for this callback.
              (default: no template)
            :param template_opts: A dict with additional template parameters.
            :param name: The name for this route. (default: None)
            :param callback: If set, the route decorator is directly applied
              to the callback and the callback is returned instead. This
              equals ``Mole.route(...)(callback)``.
        """
        # @route can be used without any parameters
        if callable(path): path, callback = None, path
        # Build up the list of decorators
        decorators = makelist(decorate)
        if template:     decorators.insert(0, view(template, **template_opts))
        if not no_hooks: decorators.append(self._add_hook_wrapper)
        #decorators.append(partial(self.apply_plugins, skiplist))
        def wrapper(func):
            for rule in makelist(path) or yieldroutes(func):
                for verb in makelist(method):
                    if static:
                        rule = rule.replace(':','\\:')
                        utils.depr("Use backslash to escape ':' in routes.")
                    #TODO: Prepare this for plugins
                    self.router.add(rule, verb, len(self.routes), name=name)
                    self.routes.append((func, decorators))
            return func
        return wrapper(callback) if callback else wrapper

    def _add_hook_wrapper(self, func):
        ''' Add hooks to a callable. See #84 '''
        @functools.wraps(func)
        def wrapper(*a, **ka):
            for hook in self.hooks['before_request']: hook()
            response.output = func(*a, **ka)
            for hook in self.hooks['after_request']: hook()
            return response.output
        return wrapper

    def get(self, path=None, method='GET', **kargs):
        """ Decorator: Bind a function to a GET request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def post(self, path=None, method='POST', **kargs):
        """ Decorator: Bind a function to a POST request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def put(self, path=None, method='PUT', **kargs):
        """ Decorator: Bind a function to a PUT request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def delete(self, path=None, method='DELETE', **kargs):
        """ Decorator: Bind a function to a DELETE request path.
            See :meth:'route' for details. """
        return self.route(path, method, **kargs)

    def error(self, code=500):
        """ Decorator: Register an output handler for a HTTP error code"""
        def wrapper(handler):
            self.error_handler[int(code)] = handler
            return handler
        return wrapper

    def hook(self, name):
        """ Return a decorator that adds a callback to the specified hook. """
        def wrapper(func):
            self.add_hook(name, func)
            return func
        return wrapper

    def add_hook(self, name, func):
        ''' Add a callback from a hook. '''
        if name not in self.hooks:
            raise ValueError("Unknown hook name %s" % name)
        if name in ('after_request'):
            self.hooks[name].insert(0, func)
        else:
            self.hooks[name].append(func)

    def remove_hook(self, name, func):
        ''' Remove a callback from a hook. '''
        if name not in self.hooks:
            raise ValueError("Unknown hook name %s" % name)
        self.hooks[name].remove(func)

    def handle(self, environ):
        """ Execute the handler bound to the specified url and method and return
        its output. If catchall is true, exceptions are catched and returned as
        HTTPError(500) objects. """
        if not self.serve:
            return HTTPError(503, "Server stopped")
        try:
            handler, args = self.match(environ)
            return handler(**args)
        except HTTPResponse, e:
            return e
        except Exception, e:
            import traceback;traceback.print_exc()
            if isinstance(e, (KeyboardInterrupt, SystemExit, MemoryError))\
            or not self.catchall:
                raise
            return HTTPError(500, 'Unhandled exception', e, format_exc(10))
class JsonProxyRestHandler(splunk.rest.BaseRestHandler):
    def __init__(self, *args, **kwargs):
        super(JsonProxyRestHandler, self).__init__(*args, **kwargs)

        self.router = Router()

        self.router.add(
            Route('/search/jobs/<sid>/control', {"POST": self.job_control},
                  'job_control'))
        self.router.add(
            Route('/search/jobs/<sid>/<data_source>', {"GET": self.job_data},
                  'job_data'))
        self.router.add(
            Route('/search/jobs/<sid>', {
                "GET": self.eai,
                "DELETE": self.delete_job
            }, 'job_info'))
        self.router.add(
            Route('/search/jobs', {
                "GET": self.eai,
                "POST": self.create_job
            }, 'jobs'))
        self.router.add(
            Route('/search/parser', self.parse_query, 'parse_query'))
        self.router.add(Route('/search/typeahead', self.typeahead,
                              'typeahead'))
        self.router.add(
            Route(
                '/search/tags/<name>', {
                    "GET": self.eai,
                    "DELETE": self.modify_or_delete_tag,
                    "POST": self.modify_or_delete_tag
                }, 'tag_info'))
        self.router.add(
            Route('/properties/<file>/<stanza>', self.properties_stanza,
                  'properties_stanza_info'))
        self.router.add(
            Route('/properties/<file>/<stanza>/<key>',
                  self.properties_stanza_key, 'properties_stanza_key'))
        self.router.add(
            Route('/receivers/simple', self.http_simple_input,
                  'http_simple_input'))
        self.router.add(Route('/auth/login', {"POST": self.auth}, 'auth'))
        self.router.add(Route('/<:.*>', self.eai, 'eai'))

    # UNDONE
    # This allows us to use basic auth, but it's not the ideal way to do this.
    # The problem is that we want to be able to reuse the code in splunk.rest.simpleRequest,
    # but that code does not allow us to set headers. As such, we have to create this wrapper
    # class.
    def wrap_http(self):
        is_basicauth = self.is_basicauth()
        basicauth = self.get_authorization()

        class Http(httplib2.Http):
            def request(self, *args, **kwargs):
                if is_basicauth and kwargs.has_key("headers"):
                    kwargs["headers"]["Authorization"] = basicauth

                return super(Http, self).request(*args, **kwargs)

        return Http

    def extract_path(self):
        self.scrubbed_path = self.request['path'].replace(
            "/services/json/v1", "")
        if re.match(r"^/servicesNS/[^/]*/[^/]*", self.scrubbed_path):
            self.scrubbed_path = re.sub(r"^(/servicesNS/[^/]*/[^/]*)(/.*)",
                                        r"\2", self.scrubbed_path)
        elif re.match(r"^/services/.*", self.scrubbed_path):
            self.scrubbed_path = re.sub(r"^(/services)(/.*)", r"\2",
                                        self.scrubbed_path)

        if self.scrubbed_path.endswith("/"):
            self.scrubbed_path = self.scrubbed_path[:-1]

        if self.scrubbed_path.endswith("?"):
            self.scrubbed_path = self.scrubbed_path[:-1]

    def extract_sessionKey(self):
        self.sessionKey = self.request["headers"].get(
            "authorization", "").replace("Splunk", "").strip() or None

    def extract_origin(self):
        if self.request["headers"].has_key(REMOTEORIGIN_HEADER):
            parsed = urlparse(self.request["headers"][REMOTEORIGIN_HEADER])
            self.remote_origin = parsed.netloc.replace(":" + str(parsed.port),
                                                       "")
        else:
            self.remote_origin = self.request["remoteAddr"]

    def extract_allowed_domains(self):
        self.allowed_domains = None

        self.settings = splunk.clilib.cli_common.getConfStanza(
            CONF_FILE, SETTINGS_STANZA)
        self.allowed_domains = map(
            lambda s: s.strip(),
            self.settings.get(ALLOWED_DOMAINS_KEY).split(","))

    def is_basicauth(self):
        return self.request["headers"].get("authorization",
                                           "").startswith("Basic ")

    def get_authorization(self):
        return self.request["headers"].get("authorization", "")

    def get_origin_error(self):
        output = ODataEntity()
        output.messages.append({
            "type":
            "HTTP",
            "text":
            "Origin '%s' is not allowed. Please check json.conf" %
            self.remote_origin
        })

        return 403, self.render_odata(output)

    def handle(self):
        output = ODataEntity()
        status = 500

        try:
            self.extract_path()
            self.extract_origin()
            self.extract_sessionKey()
            self.extract_allowed_domains()

            # Get the appropriate handler
            handler, args, kwargs = self.router.match(self.scrubbed_path)

            # Check to see if we are in the list of allowed domains
            if not self.remote_origin in self.allowed_domains:
                status, content = self.get_origin_error()
            else:
                if isinstance(handler, dict):
                    if handler.has_key(self.method):
                        handler = handler[self.method]
                    else:
                        self.set_response(404, "")
                        return

                status, content = handler(*args, **kwargs)
        except splunk.RESTException, e:
            responseCode = e.statusCode
            output.messages.append({
                'type': 'HTTP',
                'text': '%s %s' % (e.statusCode, e.msg)
            })
            if hasattr(e, 'extendedMessages') and e.extendedMessages:
                for message in e.extendedMessages:
                    output.messages.append(message)

            content = self.render_odata(output)
        except Exception, e:
            status = 500
            output.messages.append({'type': 'ERROR', 'text': '%s' % e})

            content = self.render_odata(output)
            raise