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
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