blob: d22165f1e7d074855d1477469ccf7fa9e0dfe17a [file] [log] [blame]
#!/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 Apache Warble.
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
WARBLE_VERSION = '0.1.0'
# Compile valid API URLs from the pages library
urls = []
if __name__ != '__main__':
import pages
for page in pages.handlers:
urls.append((r"^(/api/%s)(/.+)?$" % page, pages.handlers[page].run))
# Load Warble master configuration
config = yaml.load(open("yaml/warble.yaml"))
# Instantiate database connections
DB = plugins.database.WarbleDatabase(config)
# Load Open API specifications
WarbleOpenAPI = plugins.openapi.OpenAPI("yaml/openapi.yaml")
class WarbleHTTPError(Exception):
def __init__(self, code, message):
self.code = code
self.message = message
class WarbleAPIWrapper:
"""
Middleware wrapper for exceptions in the application
"""
def __init__(self, path, func):
self.func = func
self.API = WarbleOpenAPI
self.path = path
self.exception = WarbleHTTPError
def __call__(self, environ, start_response, session):
"""Run the function, return response OR return stacktrace"""
response = None
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,
'server': "Apache Warble/%s" % WARBLE_VERSION
})
return
# Validate URL against OpenAPI specs
try:
self.API.validate(environ['REQUEST_METHOD'], self.path, formdata)
except plugins.openapi.OpenAPIException as err:
session.headers.append(('Content-Type', 'application/json'))
start_response('400 Invalid request',
session.headers)
yield json.dumps({
"code": 400,
"reason": err.message,
'server': "Apache Warble/%s" % WARBLE_VERSION
})
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 WarbleHTTPError as err:
errHeaders = {
403: '403 Authentication failed',
404: '404 Resource not found',
444: '444 Empty response',
499: '499 Required Token Missing',
500: '500 Internal Server Error',
501: '501 Gateway error'
}
errHeader = errHeaders[err.code] if err.code in errHeaders else "400 Bad request"
session.headers.append(('Content-Type', 'application/json'))
start_response(errHeader, session.headers)
yield json.dumps({
"code": err.code,
"reason": err.message,
'server': "Apache Warble/%s" % WARBLE_VERSION
}, indent = 4) + "\n"
return
except:
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:
session.headers.append(('Content-Type', 'application/json'))
start_response('500 Internal Server Error',
session.headers)
except:
pass
yield json.dumps({
"code": "500",
"reason": '\n'.join(traceback_output),
'server': "Apache Warble/%s" % WARBLE_VERSION
})
def fourohfour(environ, start_response):
"""A very simple 404 handler"""
start_response("404 Not Found", [
('Content-Type', 'application/json')])
yield json.dumps({
"code": 404,
"reason": "API endpoint not found",
'server': "Apache Warble/%s" % WARBLE_VERSION
}, 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 = WarbleAPIWrapper(path, function)
session = plugins.session.WarbleSession(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:
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__':
WarbleOpenAPI.toHTML()