diff --git a/demo/app.py b/demo/app.py index c0372e0..5966b44 100644 --- a/demo/app.py +++ b/demo/app.py @@ -1,10 +1,9 @@ import pigeon.core.server as server -from pigeon.conf.settings import Settings -import settings as local +import settings as settings def run(): server.start( - Settings.from_settings(local) + settings_used=settings ) diff --git a/demo/settings.py b/demo/settings.py index 4b41b77..8e648bd 100755 --- a/demo/settings.py +++ b/demo/settings.py @@ -1,14 +1,15 @@ import pathlib -import views, errors +import views +import errors BASE_DIR = pathlib.Path(__file__).parent.resolve() # LOGGIN VERBOSITY -VERBOSITY = 2 +VERBOSITY = 4 # ADDRESS AND PORT ADDRESS = '' -PORT = 80 +PORT = 81 # VIEWS urls = { @@ -37,7 +38,7 @@ MEDIA_FILES_DIR = BASE_DIR / 'media/' # TEMPLATES -TEMPLATES_DIR = 'templates/' +TEMPLATES_DIR = BASE_DIR / 'templates/' # HTTPS USE_HTTPS = False diff --git a/src/pigeon/conf/__init__.py b/src/pigeon/conf/__init__.py index 92079e1..b1b7b2e 100644 --- a/src/pigeon/conf/__init__.py +++ b/src/pigeon/conf/__init__.py @@ -1 +1,2 @@ from pigeon.conf.settings import Settings +settings = Settings() diff --git a/src/pigeon/conf/settings.py b/src/pigeon/conf/settings.py index e0b4c09..07fb077 100644 --- a/src/pigeon/conf/settings.py +++ b/src/pigeon/conf/settings.py @@ -1,11 +1,11 @@ from pathlib import Path -import pigeon.default.settings as default class Settings: - def __init__(self, verbosity: int, address: str, port: int, allowed_hosts: list, urls: dict, errors: dict, cors: tuple, - static: tuple, media: tuple, templates_dir, https: tuple, - mime: dict): + def __init__(self, verbosity: int = None, address: str = None, port: int = None, allowed_hosts: list = None, + allowed_methods: list = None, urls: dict = None, errors: dict = None, + cors: tuple = (None, None, None, None, None), static: tuple = (None, None), media: tuple = (None, None), + templates_dir=None, https: tuple = (None, None, None, None), mime: dict = None): # logging self.verbosity = verbosity @@ -13,17 +13,18 @@ def __init__(self, verbosity: int, address: str, port: int, allowed_hosts: list, self.address = (address, port) self.allowed_hosts = allowed_hosts + self.allowed_methods = allowed_methods # cors self.cors_allowed_origins = cors[0] self.cors_allow_creds = cors[1] - self.cors_allow_headers = cors[2] - self.cors_allow_methods = cors[3] + self.cors_allowed_headers = cors[2] + self.cors_allowed_methods = cors[3] self.cors_max_age = cors[4] # views self.views = urls - self.errors = errors + self.errors = errors or dict() # static self.static_url_base = static[0] @@ -45,49 +46,43 @@ def __init__(self, verbosity: int, address: str, port: int, allowed_hosts: list, # mime self.supported_mimetypes = mime - @classmethod - def from_settings(cls, local): - return Settings( - verbosity=getattr(local, 'VERBOSITY', 2), - address=local.ADDRESS, - port=local.PORT, - allowed_hosts=local.ALLOWED_HOSTS, - urls=local.urls, - errors={**default.errors, **getattr(local, 'errors', dict())}, - cors=( - getattr(local, 'CORS_ALLOWED_ORIGINS', []), - getattr(local, 'CORS_ALLOW_CREDENTIALS', False), - getattr(local, 'CORS_ALLOW_HEADERS', ['Content-Type']), - getattr(local, 'CORS_ALLOW_METHODS', ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS']), - getattr(local, 'CORS_MAX_AGE', 1200) - ), - static=( - getattr(local, 'STATIC_URL_BASE', None), - getattr(local, 'STATIC_FILES_DIR', None), - ), - media=( - getattr(local, 'MEDIA_URL_BASE', None), - getattr(local, 'MEDIA_FILES_DIR', None), - ), - templates_dir=getattr(local, 'TEMPLATES_DIR', None), - https=( - getattr(local, 'USE_HTTPS', False), - getattr(local, 'CERTIFICATE_PATH', ''), - getattr(local, 'PRIVATE_KEY_PATH', ''), - getattr(local, 'PRIVATE_KEY_PASSWD', ''), - ), - mime=(getattr(local, 'SUPPORTED_MIMETYPES', default.SUPPORTED_MIMETYPES)), - ) + def override(self, local): + check = lambda property_name, current_value: getattr(local, property_name, current_value) + self.verbosity = check('VERBOSITY', self.verbosity) + self.address = (local.ADDRESS, local.PORT) + self.allowed_hosts = local.ALLOWED_HOSTS + self.allowed_methods = check('ALLOWED_METHODS', self.allowed_hosts) + self.views = local.urls + self.errors = {**self.errors, **check( 'errors', dict())} -settings_used: Settings + # CORS + self.cors_allowed_origins = check('CORS_ALLOWED_ORIGINS', self.cors_allowed_origins) + self.cors_allow_creds = check('CORS_ALLOW_CREDENTIALS', self.cors_allow_creds) + self.cors_allowed_headers = check('CORS_ALLOWED_HEADERS', self.cors_allowed_headers) + self.cors_allowed_methods = check('CORS_ALLOWED_METHODS', self.cors_allowed_methods) + self.cors_max_age = check('CORS_MAX_AGE', self.cors_max_age) + # STATIC + self.static_url_base = check('STATIC_URL_BASE', self.static_url_base) + self.static_files_dir = check('STATIC_FILES_DIR', self.static_files_dir) + self.static_files_dir = Path(self.static_files_dir) if self.static_files_dir else None -def use(settings: Settings) -> None: - global settings_used - settings_used = settings + # MEDIA + self.media_url_base = check('MEDIA_URL_BASE', self.media_url_base) + self.media_files_dir = check('MEDIA_FILES_DIR', self.media_files_dir) + self.media_files_dir = Path(self.media_files_dir) if self.media_files_dir else None + # TEMPLATING + self.templates_dir = check('TEMPLATES_DIR', self.templates_dir) + self.templates_dir = Path(self.templates_dir) if self.templates_dir else None -def get() -> Settings: - global settings_used - return settings_used + # HTTPS + self.use_https = check('USE_HTTPS', self.use_https) + self.https_cert_path = check('CERTIFICATE_PATH', self.https_cert_path) + self.https_cert_path = Path(self.https_cert_path) if self.https_cert_path else None + self.https_privkey_path = check('PRIVATE_KEY_PATH', self.https_privkey_path) + self.https_privkey_path = Path(self.https_privkey_path) if self.https_privkey_path else None + self.https_privkey_passwd = check('PRIVATE_KEY_PASSWD', self.https_privkey_passwd) + + self.supported_mimetypes = check('SUPPORTED_MIMETYPES', self.supported_mimetypes) diff --git a/src/pigeon/core/__init__.py b/src/pigeon/core/__init__.py index 3347005..238abc8 100644 --- a/src/pigeon/core/__init__.py +++ b/src/pigeon/core/__init__.py @@ -1,4 +1,3 @@ -import pigeon.core.access_control as access_control import pigeon.core.handler as handler import pigeon.core.secure as secure import pigeon.core.server as server diff --git a/src/pigeon/core/handler.py b/src/pigeon/core/handler.py index 06424da..11c258a 100644 --- a/src/pigeon/core/handler.py +++ b/src/pigeon/core/handler.py @@ -1,11 +1,14 @@ import socket -import pigeon.conf.settings as _settings +from pigeon.conf import settings import pigeon.middleware as middleware from pigeon.utils.logger import create_log from pigeon.http import HTTPRequest, HTTPResponse +from pigeon.files.static import handle_static_request +from pigeon.files.media import handle_media_request +from pigeon.http.common import error log = create_log('HANDLER', 'cyan') -settings = _settings.get() + def receive_data(client_sock: socket.socket, size:int = 4096): @@ -54,7 +57,7 @@ def handle_connection(client_sock: socket.socket, client_address: tuple): log(3, f'RESPONSE SENT') # do not keep connection open on error - if response.is_error(): + if response.is_error: break # client asks to terminate connection diff --git a/src/pigeon/core/server.py b/src/pigeon/core/server.py index 1b84138..518e897 100644 --- a/src/pigeon/core/server.py +++ b/src/pigeon/core/server.py @@ -1,8 +1,9 @@ import socket -import pigeon.conf.settings as _settings -import pigeon.core.secure as secure -from pigeon.utils.logger import create_log +from pigeon.conf import settings +import pigeon.default.settings as default import pigeon.utils.logger as logger +from pigeon.utils.logger import create_log +import pigeon.core.secure as secure import pigeon.core.handler as handler import pigeon.default.errors as default_errors import pigeon.files.static as static @@ -12,11 +13,10 @@ log = create_log('SERVER', 'white') -def start(settings_used: _settings.Settings): +def start(settings_used): # configure settings - _settings.use(settings_used) - - settings = settings_used + settings.override(default) + settings.override(settings_used) # set verbosity for logger @@ -34,10 +34,10 @@ def start(settings_used: _settings.Settings): log(2, 'LOADING TEMPLATES') templater.load() - serve(settings) + serve() -def serve(settings: _settings.Settings): +def serve(): log(2, f'ADDRESS: {settings.address[0] if settings.address[0] else "ANY"}') log(2, f'PORT: {settings.address[1]}') diff --git a/src/pigeon/default/errors.py b/src/pigeon/default/errors.py index ad64c9d..13c5652 100644 --- a/src/pigeon/default/errors.py +++ b/src/pigeon/default/errors.py @@ -1,8 +1,8 @@ -from pigeon.http import HTTPResponse, HTTPRequest, JSONResponse +from pigeon.http import HTTPResponse, HTTPRequest, JSONResponse -def fallback(request: HTTPRequest, code: int): +def fallback(request: HTTPRequest | None, code: int): """ Fallback for when no """ - return JSONResponse(data={'error':f'invalid request: {request.path}'}, status=code) + return JSONResponse(data={'error': f'error{": " + request.path if request else ""} {code}'}, status=code) diff --git a/src/pigeon/default/settings.py b/src/pigeon/default/settings.py index b46e725..45e2070 100644 --- a/src/pigeon/default/settings.py +++ b/src/pigeon/default/settings.py @@ -11,6 +11,9 @@ # ALLOWED HOSTS ALLOWED_HOSTS = None +# ALLOWED METHODS +ALLOWED_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS'] + # VIEWS urls = { @@ -20,14 +23,11 @@ 000: fallback, } -# ALLOWED METHODS -ALLOWED_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS'] - # CORS CORS_ALLOWED_ORIGINS = [] CORS_ALLOW_CREDENTIALS = False -CORS_ALLOW_HEADERS = ['Content-Type'] -CORS_ALLOW_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS'] +CORS_ALLOWED_HEADERS = ['Content-Type'] +CORS_ALLOWED_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS'] CORS_MAX_AGE = 1200 # STATIC diff --git a/src/pigeon/files/__init__.py b/src/pigeon/files/__init__.py index 17f8a39..4e3e8ff 100644 --- a/src/pigeon/files/__init__.py +++ b/src/pigeon/files/__init__.py @@ -1,2 +1,2 @@ -import pigeon.files.media as media -import pigeon.files.static as static +from pigeon.files.media import handle_media_request +from pigeon.files.static import handle_static_request diff --git a/src/pigeon/files/media.py b/src/pigeon/files/media.py index 23f6f1f..945bb38 100644 --- a/src/pigeon/files/media.py +++ b/src/pigeon/files/media.py @@ -1,4 +1,4 @@ -import pigeon.conf.settings as _settings +from pigeon.conf import settings from pigeon.http import HTTPRequest, HTTPResponse from pigeon.http.common import error, status from pathlib import Path @@ -6,8 +6,6 @@ import gzip import os -settings = _settings.get() - def fetch_file(local_path: Path, encodings): """ diff --git a/src/pigeon/files/static.py b/src/pigeon/files/static.py index d913e04..9066b65 100644 --- a/src/pigeon/files/static.py +++ b/src/pigeon/files/static.py @@ -1,4 +1,4 @@ -import pigeon.conf.settings as _settings +from pigeon.conf import settings from pigeon.http import HTTPRequest, HTTPResponse from pigeon.http.common import error from pathlib import Path @@ -7,7 +7,7 @@ import os loaded_files = dict() -settings = _settings.get() + def load(): diff --git a/src/pigeon/http/common.py b/src/pigeon/http/common.py index 49bf0fa..5565cc0 100644 --- a/src/pigeon/http/common.py +++ b/src/pigeon/http/common.py @@ -1,8 +1,6 @@ -import pigeon.conf.settings as _settings +from pigeon.conf import settings from http import HTTPStatus -settings = _settings.get() - def error(code: int, request): """ @@ -10,10 +8,10 @@ def error(code: int, request): """ # if a specific error view for the error code exists if code in settings.errors: - return settings.errors[code](request) + return settings.errors[code](request=request) # otherwise just return a standard error page but with the code provided else: - return settings.errors[000](request, code) + return settings.errors[000](request=request, code=code) def status(code): diff --git a/src/pigeon/http/parsing/parser.py b/src/pigeon/http/parsing/parser.py index 93b5f03..20f43d1 100644 --- a/src/pigeon/http/parsing/parser.py +++ b/src/pigeon/http/parsing/parser.py @@ -6,11 +6,14 @@ -def parse(request: str) -> HTTPRequest: +def parse(request: bytes) -> HTTPRequest: """ Parses a string representation of an http request and creates a valid HTTPRequest object from it. """ + # decode request + request = str(request, 'ascii') + # split into request line and message request_line, message_raw = (request.split('\r\n', 1)+[''])[:2] diff --git a/src/pigeon/middleware/components/connection.py b/src/pigeon/middleware/components/connection.py index d89443b..d2ee98d 100644 --- a/src/pigeon/middleware/components/connection.py +++ b/src/pigeon/middleware/components/connection.py @@ -9,16 +9,19 @@ class ConnectionComponent(comp.MiddlewareComponent): @classmethod def postprocess(cls, response: HTTPResponse, request: HTTPRequest) -> HTTPResponse | int: # do not close connection if client requests to keep it alive - if cls.is_keep_alive(request=request): - response.set_headers(headers={'Connection': 'keep-alive'}) - response.set_headers(headers={'Connection': 'close'}) + response.set_headers(headers={'Connection': 'keep-alive' if request.keep_alive else 'close'}) return response + @classmethod + def preprocess(cls, request: HTTPRequest) -> HTTPRequest | int: + # set keep-alive property for HTTPRequest object + request.keep_alive = cls.is_keep_alive(request=request) + return request @classmethod def is_keep_alive(cls, request: HTTPRequest) -> bool: """ Checks if the Host header in the request has a valid hostname. """ - return request.headers('connection') == 'keep-alive' \ No newline at end of file + return request.headers('connection') == 'keep-alive' diff --git a/src/pigeon/middleware/components/method.py b/src/pigeon/middleware/components/method.py index fd58665..1eaaf38 100644 --- a/src/pigeon/middleware/components/method.py +++ b/src/pigeon/middleware/components/method.py @@ -4,7 +4,6 @@ - class MethodComponent(comp.MiddlewareComponent): @classmethod def preprocess(cls, request: HTTPRequest) -> HTTPRequest | int: diff --git a/src/pigeon/middleware/pipe.py b/src/pigeon/middleware/pipe.py index b4dd475..9c0374f 100644 --- a/src/pigeon/middleware/pipe.py +++ b/src/pigeon/middleware/pipe.py @@ -3,14 +3,12 @@ from pigeon.files import handle_media_request, handle_static_request from pigeon.http import HTTPRequest, HTTPResponse, error import pigeon.conf.middleware as middleware -import pigeon.middleware.processing as processing import pigeon.http.parsing.parser as parser log = create_log('MIDDLEWARE', 'green') - -def preprocess(raw: str) -> HTTPRequest | int: +def preprocess(raw: bytes) -> HTTPRequest | int: """ Tries to parse the raw request and checks whether the request is valid. If the request is invalid or could not be parsed correctly, an http status error code will be returned. @@ -29,7 +27,7 @@ def preprocess(raw: str) -> HTTPRequest | int: # try processing the request try: - return processing.PROCESSORS[request.protocol].preprocess(request=request) + return middleware.PROCESSORS[request.protocol].preprocess(request=request) except Exception: log(1, f'MIDDLEWARE FAILED WHEN PREPROCESSING REQUEST - SKIPPING') return 500 @@ -42,24 +40,24 @@ def process(request: HTTPRequest | int) -> HTTPResponse: # if request is of type integer, the preprocessing failed and an error should be returned if isinstance(request, int): - # request failed - return error to client - return error(request, None) + log(2, f'PREPROCESSOR RETURNED ERROR {request}') + # request failed preprocessing - return error to client + # -> request now contains the http response status code, not the actual request + return error(code=request, request=None) # gather response for request if settings.static_url_base and request.path.startswith(settings.static_url_base): # request for static file response = handle_static_request(request) - elif settings.media_url_base and request.path.startswith(settings.media_url_base): # request for media file response = handle_media_request(request) - elif request.path in settings.views: # views response = settings.views[request.path](request) else: # page does not exist - return error(404, request) + return error(code=404, request=request) return response @@ -69,13 +67,18 @@ def postprocess(request: HTTPRequest, response: HTTPResponse) -> HTTPResponse: Modifies some components of the response such as headers to fit in with server-side policies (e.g. CORS). """ - # if preprocessor failed response cannot reliably be postprocessed + # check if preprocessor exited correctly if isinstance(request, int): - return response + request = None # try processing the request try: - return processing.PROCESSORS[request.protocol].postprocess(response=response, request=request) + response = middleware.PROCESSORS[request.protocol].postprocess(response=response, request=request) + # request failed postprocessing - return error to client + # -> response now contains the http response status code, not the actual response + if isinstance(response, int): + return error(code=response, request=None) + return response except Exception: log(1, f'MIDDLEWARE FAILED WHEN POSTPROCESSING REQUEST - SKIPPING') - return 500 + return error(code=500, request=request) diff --git a/src/pigeon/middleware/processing.py b/src/pigeon/middleware/processing.py index c5cc635..2fe2daa 100644 --- a/src/pigeon/middleware/processing.py +++ b/src/pigeon/middleware/processing.py @@ -18,8 +18,8 @@ class Owl(Processor): @classmethod def preprocess(cls, request: HTTPRequest) -> HTTPRequest | int: # run every middleware preprocess comnponent on request - for component in middleware.POSTPROCESSING_COMPONENTS: - request = component(request) + for component in middleware.PREPROCESSING_COMPONENTS: + request = component.preprocess(request=request) if isinstance(request, int): return request return request @@ -28,10 +28,10 @@ def preprocess(cls, request: HTTPRequest) -> HTTPRequest | int: def postprocess(cls, response: HTTPResponse, request: HTTPRequest) -> HTTPResponse | int: # run every middleware postprocess component on response for component in middleware.POSTPROCESSING_COMPONENTS: - response = component(request, response) + response = component.postprocess(response=response, request=request) if isinstance(response, int): - return error(response) - return response + return response + return response class Raven(Processor): diff --git a/src/pigeon/scripts/_resources/app.py b/src/pigeon/scripts/_resources/app.py index c0372e0..26e159b 100644 --- a/src/pigeon/scripts/_resources/app.py +++ b/src/pigeon/scripts/_resources/app.py @@ -1,11 +1,9 @@ import pigeon.core.server as server from pigeon.conf.settings import Settings -import settings as local +import settings def run(): - server.start( - Settings.from_settings(local) - ) + server.start(settings) if __name__ == '__main__': diff --git a/src/pigeon/templating/templater.py b/src/pigeon/templating/templater.py index b766245..13365f5 100644 --- a/src/pigeon/templating/templater.py +++ b/src/pigeon/templating/templater.py @@ -1,11 +1,10 @@ -import pigeon.conf.settings as _settings +from pigeon.conf import settings from pigeon.http import HTTPResponse from pathlib import Path from jinja2 import Environment, FileSystemLoader, select_autoescape import mimetypes -import os -settings = _settings.get() + env = None