| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| This is the main WSGI handler file for Blocky. |
| It compiles a list of valid URLs from the 'pages' library folder, |
| and if a URL matches it runs the specific submodule's run() function. It |
| also handles CGI parsing and exceptions in the applications. |
| """ |
| |
| |
| # Main imports |
| import cgi |
| import re |
| import sys |
| import traceback |
| import yaml |
| import json |
| import plugins.session |
| import plugins.database |
| import plugins.openapi |
| import plugins.worker |
| import atexit |
| import os |
| import time |
| import threading |
| |
| |
| # Hack to start first worker as a background thread |
| ppid = os.getppid() |
| now = time.time() |
| pidfile = "./blocky-bg-%s.pid" % ppid |
| background = False |
| |
| def rmpidfile(): |
| try: |
| print("Removing BG thread pid file...") |
| os.unlink(pidfile) |
| except Exception: # TODO: narrow further to expected Exceptions |
| pass |
| |
| if not os.path.exists(pidfile) or os.path.getmtime(pidfile) < (now - 60): |
| print("Starting this thread as background worker") |
| open(pidfile, "w").write(str(os.getpid())) |
| atexit.register(rmpidfile) |
| background = True |
| |
| # Compile valid API URLs from the pages library |
| # Allow backwards compatibility by also accepting .lua URLs |
| urls = [] |
| if __name__ != '__main__': |
| import pages |
| for page in pages.handlers: |
| urls.append((r"^(/api/%s)(/.+)?$" % page, pages.handlers[page].run)) |
| |
| |
| # Load Blocky master configuration |
| config = yaml.load(open("yaml/blocky.yaml")) |
| |
| # Instantiate database connections |
| DB = plugins.database.BlockyDatabase(config) |
| |
| # Load Open API specifications |
| BlockyOpenAPI = plugins.openapi.OpenAPI("yaml/openapi.yaml") |
| |
| # Start as background worker? |
| if background: |
| t = threading.Thread(target = plugins.worker.start, args = (DB, config, pidfile)) |
| t.start() |
| |
| |
| class BlockyAPIWrapper: |
| """ |
| Middleware wrapper for exceptions in the application |
| """ |
| def __init__(self, path, func): |
| self.func = func |
| self.API = BlockyOpenAPI |
| self.path = path |
| self.exception = plugins.openapi.BlockyHTTPError |
| |
| def __call__(self, environ, start_response, session): |
| """Run the function, return response OR return stacktrace""" |
| response = None |
| |
| # CORS IGNORE |
| if environ.get('REQUEST_METHOD', 'GET') == 'OPTIONS': |
| start_response('200 Fine, whatever', [ |
| ('Content-Type', 'application/json')]) |
| yield json.dumps({ |
| "code": 200, |
| "reason": "You're probably just being silly..." |
| }) |
| return |
| |
| try: |
| # Read JSON client data if any |
| try: |
| request_size = int(environ.get('CONTENT_LENGTH', 0)) |
| except (ValueError): |
| request_size = 0 |
| requestBody = environ['wsgi.input'].read(request_size) |
| formdata = {} |
| if requestBody and len(requestBody) > 0: |
| try: |
| formdata = json.loads(requestBody.decode('utf-8')) |
| |
| except json.JSONDecodeError as err: |
| start_response('400 Invalid request', [ |
| ('Content-Type', 'application/json')]) |
| yield json.dumps({ |
| "code": 400, |
| "reason": "Invalid JSON: %s" % err |
| }) |
| return |
| else: |
| # Try query string?? |
| xdata = cgi.parse_qs(environ.get('QUERY_STRING')) |
| for k, v in xdata.items(): |
| formdata[k] = v[0] |
| |
| |
| # Validate URL against OpenAPI specs |
| try: |
| self.API.validate(environ['REQUEST_METHOD'], self.path, formdata) |
| except plugins.openapi.OpenAPIException as err: |
| start_response('400 Invalid request', [ |
| ('Content-Type', 'application/json')]) |
| yield json.dumps({ |
| "code": 400, |
| "reason": err.message |
| }) |
| return |
| |
| # Call page with env, SR and form data |
| try: |
| response = self.func(self, environ, formdata, session) |
| if response: |
| for bucket in response: |
| yield bucket |
| except plugins.openapi.BlockyHTTPError as err: |
| errHeaders = { |
| 403: '403 Authentication failed', |
| 404: '404 Resource not found', |
| 500: '500 Internal Server Error', |
| 501: '501 Gateway error' |
| } |
| errHeader = errHeaders[err.code] if err.code in errHeaders else "400 Bad request" |
| start_response(errHeader, [ |
| ('Content-Type', 'application/json')]) |
| yield json.dumps({ |
| "code": err.code, |
| "reason": err.message |
| }, indent = 4) + "\n" |
| return |
| |
| except Exception: # TODO: narrow further to expected Exceptions |
| err_type, err_value, tb = sys.exc_info() |
| traceback_output = ['API traceback:'] |
| traceback_output += traceback.format_tb(tb) |
| traceback_output.append('%s: %s' % (err_type.__name__, err_value)) |
| # We don't know if response has been given yet, try giving one, fail gracefully. |
| try: |
| start_response('500 Internal Server Error', [ |
| ('Content-Type', 'application/json')]) |
| except Exception: # TODO: narrow further to expected Exceptions |
| pass |
| yield json.dumps({ |
| "API": "Blocky API 1.0", |
| "code": "500", |
| "reason": '\n'.join(traceback_output) |
| }) |
| |
| |
| def fourohfour(environ, start_response): |
| """A very simple 404 handler""" |
| start_response("404 Not Found", [ |
| ('Content-Type', 'application/json')]) |
| yield json.dumps({ |
| "API": "Blocky API 2.0", |
| "code": 404, |
| "reason": "API endpoint not found" |
| }, indent = 4) + "\n" |
| return |
| |
| |
| def application(environ, start_response): |
| """ |
| This is the main handler. Every API call goes through here. |
| Checks against the pages library, and if submod found, runs |
| it and returns the output. |
| """ |
| path = environ.get('PATH_INFO', '') |
| for regex, function in urls: |
| m = re.match(regex, path) |
| if m: |
| callback = BlockyAPIWrapper(path, function) |
| session = plugins.session.BlockySession(DB, environ, config) |
| a = 0 |
| for bucket in callback(environ, start_response, session): |
| if a == 0: |
| session.headers.append(bucket) |
| try: |
| start_response("200 Okay", session.headers) |
| except Exception: # TODO: narrow further to expected Exceptions |
| pass |
| a += 1 |
| # WSGI prefers byte strings, so convert if regular py3 string |
| if isinstance(bucket, str): |
| yield bytes(bucket, encoding = 'utf-8') |
| elif isinstance(bucket, bytes): |
| yield bucket |
| return |
| |
| for bucket in fourohfour(environ, start_response): |
| yield bytes(bucket, encoding = 'utf-8') |
| |
| |
| |
| if __name__ == '__main__': |
| BlockyOpenAPI.toHTML() |