diff --git a/http_server/izzylib/http_server/__init__.py b/http_server/izzylib/http_server/__init__.py new file mode 100644 index 0000000..7d8dfda --- /dev/null +++ b/http_server/izzylib/http_server/__init__.py @@ -0,0 +1,9 @@ +from datetime import datetime + +start_time = datetime.now() + +from .application import Application +from .config import Config +from .request import Request +from .response import Response +from .view import View diff --git a/http_server/izzylib/http_server/application.py b/http_server/izzylib/http_server/application.py new file mode 100644 index 0000000..a667580 --- /dev/null +++ b/http_server/izzylib/http_server/application.py @@ -0,0 +1,106 @@ +import multiprocessing, sanic, signal, traceback +import logging as pylog + + +from multidict import CIMultiDict +from multiprocessing import cpu_count, current_process +from urllib.parse import parse_qsl, urlparse + +from izzylib import DotDict, Path, logging +from izzylib.template import Template + +from .config import Config +from .error_handlers import GenericError, MissingTemplateError +from .middleware import AccessLog, Headers +from .view import Manifest, Style + + +log_path_ignore = [ + '/media', + '/static' +] + +log_ext_ignore = [ + 'js', 'ttf', 'woff2', + 'ac3', 'aiff', 'flac', 'm4a', 'mp3', 'ogg', 'wav', 'wma', + 'apng', 'ico', 'jpeg', 'jpg', 'png', 'svg', + 'divx', 'mov', 'mp4', 'webm', 'wmv' +] + +frontend = Path(__file__).resolve.parent.join('frontend') + +class Application(sanic.Sanic): + def __init__(self, **kwargs): + self.cfg = Config(**kwargs) + + super().__init__(self.cfg.name, request_class=self.cfg.request_class) + + for log in ['sanic.root', 'sanic.access']: + pylog.getLogger(log).setLevel(pylog.WARNING) + + self.template = Template( + self.cfg.tpl_search, + self.cfg.tpl_globals, + self.cfg.tpl_context, + self.cfg.tpl_autoescape + ) + + self.template.add_env('cfg', self.cfg) + + if self.cfg.tpl_default: + self.template.add_search_path(frontend) + self.add_class_route(Manifest) + self.add_class_route(Style) + self.static('/favicon.ico', frontend.join('static/icon64.png')) + self.static('/framework/static', frontend.join('static')) + + self.add_error_handler(MissingTemplateError) + self.add_error_handler(GenericError) + + signal.signal(signal.SIGHUP, self.finish) + signal.signal(signal.SIGINT, self.finish) + signal.signal(signal.SIGQUIT, self.finish) + signal.signal(signal.SIGTERM, self.finish) + + + def add_class_route(self, cls): + for route in cls.paths: + self.add_route(cls.as_view(), route) + + + def add_error_handler(self, handler): + handle = handler(self) + self.error_handler.add(*handle()) + + + def add_middleware(self, middleware): + mw = middleware(self) + self.register_middleware(mw.handler, mw.attach) + + + def start(self): + # register built-in middleware now so they're last in the chain + self.add_middleware(Headers) + self.add_middleware(AccessLog) + + msg = f'Starting {self.cfg.name} at {self.cfg.host}:{self.cfg.port}' + + if self.cfg.workers > 1: + msg += f' with {self.cfg.workers} workers' + + logging.info(msg) + self.run( + host = self.cfg.listen, + port = self.cfg.port, + workers = self.cfg.workers, + access_log = False, + debug = False + ) + + + def finish(self): + if self.cfg.sig_handler: + self.cfg.sig_handler(*self.cfg.sig_handler_args, **self.cfg.sig_handler_kwargs) + + self.stop() + logging.info('Bye! :3') diff --git a/http_server/izzylib/http_server/config.py b/http_server/izzylib/http_server/config.py new file mode 100644 index 0000000..0f63a92 --- /dev/null +++ b/http_server/izzylib/http_server/config.py @@ -0,0 +1,48 @@ +from izzylib import DotDict +from multiprocessing import cpu_count + +from .request import Request +from .response import Response + + +class Config(DotDict): + defaults = dict( + name = 'IzzyLib Http Server', + version = '0.0.1', + git_repo = 'https://git.barkshark.xyz/izaliamae/izzylib', + listen = 'localhost', + host = 'localhost', + web_host = 'localhost', + alt_hosts = [], + port = 8080, + proto = 'http', + workers = cpu_count(), + request_class = Request, + response_class = Response, + sig_handler = None, + sig_handler_args = [], + sig_handler_kwargs = {}, + menu = {}, + tpl_search = [], + tpl_globals = {}, + tpl_context = None, + tpl_autoescape = True, + tpl_default = True + ) + + + def __init__(self, **kwargs): + super().__init__({**self.defaults, **kwargs}) + + if kwargs.get('host') and not kwargs.get('web_host'): + self.web_host = self.host + + + def __setitem__(self, key, value): + if key not in self.defaults.keys(): + raise KeyError(f'Invalid config key {key}') + + if key == 'port' and not isinstance(value, int): + raise TypeError('Port must be an integer') + + super().__setitem__(key, value) diff --git a/http_server/izzylib/http_server/error_handlers.py b/http_server/izzylib/http_server/error_handlers.py new file mode 100644 index 0000000..7e8f54e --- /dev/null +++ b/http_server/izzylib/http_server/error_handlers.py @@ -0,0 +1,47 @@ +import traceback + +from jinja2.exceptions import TemplateNotFound + +from .response import Response + + +class GenericError: + error = Exception + + + def __init__(self, app): + self.app = app + + + def __call__(self): + return self.error, self.handler + + + def handler(self, request, exception): + response = Response(self.app, request) + + try: + status = exception.status_code + msg = str(exception) + except: + msg = f'{exception.__class__.__name__}: {str(exception)}' + status = 500 + + if status not in range(200, 499): + traceback.print_exc() + + try: + return response.error(msg, status) + + except Exception as e: + traceback.print_exc() + return response.text(f'{exception.__class__.__name__}: {msg}', status=500) + + +class MissingTemplateError(GenericError): + error = TemplateNotFound + + + def handler(self, request, exception): + logging.error('TEMPLATE_ERROR:', f'{exception.__class__.__name__}: {str(exception)}') + return request.response.html('I\'m a dingleberry and forgot to create a template for this page', 500) diff --git a/http_server/izzylib/http_server/frontend/base.css b/http_server/izzylib/http_server/frontend/base.css new file mode 100644 index 0000000..5f35466 --- /dev/null +++ b/http_server/izzylib/http_server/frontend/base.css @@ -0,0 +1,446 @@ +:root { + --text: #eee; + --hover: {{primary.desaturate(50).lighten(50)}}; + --primary: {{primary}}; + --background: {{background}}; + --ui: {{primary.desaturate(25).lighten(5)}}; + --ui-background: {{background.lighten(7.5)}}; + --shadow-color: {{black.rgba(25)}}; + --shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color); + + --negative: {{negative}}; + --negative-dark: {{negative.darken(85)}}; + --positive: {{positive}}; + --positive-dark: {{positive.darken(85)}}; + + --message: var(--positive); + --error: var(--negative); + --gap: 15px; + --easing: cubic-bezier(.6, .05, .28, .91); + --trans-speed: {{speed}}ms; +} + +body { + color: var(--text); + background-color: var(--background); + font-family: sans undertale; + font-size: 16px; + margin: 15px 0; +} + +a, a:visited { + color: var(--primary); + text-decoration: none; +} + +a:hover { + color: var(--hover); + text-decoration: underline; +} + +input:not([type='checkbox']), select, textarea { + margin: 1px 0; + color: var(--text); + background-color: var(--background); + border: 1px solid var(--background); + box-shadow: 0 2px 2px 0 var(--shadow-color); +} + +input:hover, select:hover, textarea:hover { + border-color: var(--hover); +} + +input:focus, select:focus, textarea:focus { + outline: 0; + border-color: var(--primary); +} + +details:focus, summary:focus { + outline: 0; +} + +/* Classes */ +.button { + display: inline-block; + padding: 5px; + background-color: {{primary.darken(85)}}; + text-align: center; + box-shadow: var(--shadow); +} + +.button:hover { + background-color: {{primary.darken(65)}}; + text-decoration: none; +} + +.grid-container { + display: grid; + grid-template-columns: auto; + grid-gap: var(--gap); +} + +.grid-item { + display: inline-grid; +} + +.flex-container { + display: flex; + flex-wrap; wrap; +} + +.menu { + list-style-type: none; + padding: 0; + margin: 0; +} + +.menu li { + display: inline-block; + text-align: center; + min-width: 60px; + background-color: {{background.lighten(20)}}; +} + +.menu li a { + display: block; + padding-left: 5px; + padding-right: 5px; +} + +.menu li:hover { + background-color: {{primary.lighten(25).desaturate(25)}}; +} + +.menu li a:hover { + text-decoration: none; + color: {{primary.darken(90).desaturate(50)}}; +} + +.section { + padding: 8px; + background-color: var(--ui-background); + box-shadow: var(--shadow); +} + +.shadow { + box-shadow: 0 4px 4px 0 var(--shadow-color), 3px 0 4px 0 var(--shadow-color); +} + +.message { + line-height: 2em; + display: block; +} + +/* # this is kinda hacky and needs to be replaced */ +.tooltip:hover::after { + position: relative; + padding: 8px; + bottom: 35px; + border-radius: 5px; + white-space: nowrap; + border: 1px solid var(--text); + color: var(--text); + background-color: {{primary.desaturate(50).darken(75)}}; + box-shadow: var(--shadow); + /*z-index: -1;*/ +} + + +/* ids */ +#title { + font-size: 36px; + font-weight: bold; + text-align: center; +} + +#message, #error { + padding: 10px; + color: var(--background); + margin-bottom: var(--gap); + text-align: center; +} + +#message { + background-color: var(--message); +} + +#error { + background-color: var(--error); +} + +#body { + width: 790px; + margin: 0 auto; +} + +#header { + display: flex; + margin-bottom: var(--gap); + text-align: center; + font-size: 2em; + line-height: 40px; + font-weight: bold; +} + +#header > div { + /*display: inline-block;*/ + height: 40px; +} + +#header .page-title { + text-align: {% if menu_left %}right{% else %}left{% endif %}; + white-space: nowrap; + overflow: hidden; + width: 100%; +} + +#content-body .title { + text-align: center; + font-size: 1.5em; + font-weight: bold; + color: var(--primary) +} + +#footer { + margin-top: var(--gap); + display: flex; + grid-gap: 5px; + font-size: 0.80em; + line-height: 20px; +} + +#footer > div { + height: 20px; +} + +#footer .avatar img { + margin: 0 auto; +} + +#footer .user { + white-space: nowrap; + overflow: hidden; + width: 100%; +} + +#footer .source { + white-space: nowrap; +} + +{% for name in cssfiles %} + {% include 'style/' + name + '.css' %} +{% endfor %} + + +/* responsive design */ +@media (max-width: 810px) { + body { + margin: 0; + } + + #body { + width: auto; + } +} + +@media (max-width: 610px) { + .settings .grid-container { + grid-template-columns: auto; + } + + .settings .label { + text-align: center; + } +} + +/* Main menu */ +#btn { + cursor: pointer; + transition: left 500ms var(--easing); +} + +#btn { + transition: background-color var(--trans-speed); + width: 55px; + margin-left: var(--gap); + background-image: url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/menu.svg'); + background-size: 50px; + background-position: center center; + background-repeat: no-repeat; +} + +#btn div { + transition: transform var(--trans-speed) ease, opacity var(--trans-speed), background-color var(--trans-speed); +} + +#btn.active { + margin-left: 0; + position: fixed; + z-index: 5; + top: 12px; + {% if menu_left %}right: calc(100% - 250px + 12px){% else %}right: 12px;{% endif %}; + background-color: {{primary.darken(75)}}; + color: {{background}}; +} + +/*#btn.active div { + width: 35px; + height: 2px; + margin-bottom: 8px; +}*/ + +#btn.active:parent { + grid-template-columns: auto; +} + + +#menu { + position: fixed; + z-index: 4; + overflow: auto; + top: 0px; + opacity: 0; + padding: 20px 0px; + width: 250px; + height: 100%; + transition: all var(--trans-speed) ease; + {% if menu_left %}left{% else %}right{% endif %}: -250px; +} + +#menu.active { + {% if menu_left %}left{% else %}right{% endif %}: 0; + opacity: 1; +} + +#menu #items { + /*margin-top: 50px;*/ + margin-bottom: 30px; +} + +#menu a:hover { + text-decoration: none; +} + +#menu { + font-weight: bold; +} + +#menu .item { + display: block; + position: relative; + font-size: 2em; + transition: all var(--trans-speed); + padding-left: 20px; +} + +#menu .title-item { + color: var(--primary); +} + +#items .sub-item { + padding-left: 40px; +} + +#items .item:not(.title-item):hover { + padding-left: 40px; +} + +#items .sub-item:hover { + padding-left: 60px !important; +} + +/*#menu details .item:hover { + padding-left: 60px; +}*/ + +#items summary { + cursor: pointer; + color: var(--primary); +} + +#items details[open]>.item:not(details) { + animation-name: fadeInDown; + animation-duration: var(--trans-speed); +} + + +#items summary::-webkit-details-marker { + display: none; +} + +#items details summary:after { + content: " +"; +} + +#items details[open] summary:after { + content: " -"; +} + + +#btn, #btn * { + will-change: transform; +} + +#menu { + will-change: transform, opacity; +} + + +@keyframes fadeInDown { + 0% { + opacity: 0; + transform: translateY(-1.25em); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + + +/* scrollbar */ +body {scrollbar-width: 15px; scrollbar-color: var(--primary) {{background.darken(10)}};} +::-webkit-scrollbar {width: 15px;} +::-webkit-scrollbar-track {background: {{background.darken(10)}};} +/*::-webkit-scrollbar-button {background: var(--primary);} +::-webkit-scrollbar-button:hover {background: var(--text);}*/ +::-webkit-scrollbar-thumb {background: var(--primary);} +::-webkit-scrollbar-thumb:hover {background: {{primary.lighten(25)}};} + + +/* page font */ +@font-face { + font-family: 'sans undertale'; + src: local('Nunito Sans Bold'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-SemiBold.woff2') format('woff2'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-SemiBold.ttf') format('ttf'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'sans undertale'; + src: local('Nunito Sans Light Italic'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-ExtraLightItalic.woff2') format('woff2'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-ExtraLightItalic.ttf') format('ttf'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'sans undertale'; + src: local('Nunito Sans Bold Italic'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Italic.woff2') format('woff2'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Italic.ttf') format('ttf'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'sans undertale'; + src: local('Nunito Sans Light'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Light.woff2') format('woff2'), + url('{{cfg.proto}}://{{cfg.web_host}}/framework/static/nunito/NunitoSans-Light.ttf') format('ttf'); + font-weight: normal; + font-style: normal; +} diff --git a/http_server/izzylib/http_server/frontend/base.haml b/http_server/izzylib/http_server/frontend/base.haml new file mode 100644 index 0000000..5bf8422 --- /dev/null +++ b/http_server/izzylib/http_server/frontend/base.haml @@ -0,0 +1,42 @@ + +%html + %head + %title << {{cfg.name}}: {{page}} + %link rel='stylesheet' type='text/css' href='{{cfg.proto}}://{{cfg.web_host}}/framework/style.css' + %link rel='manifest' href='{{cfg.proto}}://{{cfg.web_host}}/framework/manifest.json' + %meta charset='UTF-8' + %meta name='viewport' content='width=device-width, initial-scale=1' + + %body + #body + #header.flex-container + -if menu_left + #btn.section + .page-title.section -> %a.title href='{{cfg.proto}}://{{cfg.web_host}}/' << {{cfg.name}} + + -else + .page-title.section -> %a.title href='{{cfg.proto}}://{{cfg.web_host}}/' << {{cfg.name}} + #btn.section + + -if message + #message.section << {{message}} + + -if error + #error.secion << {{error}} + + #menu.section + .title-item.item << Menu + #items + -for label, path in cfg.menu.items() + .item -> %a href='{{cfg.proto}}://{{cfg.web_host}}{{path}}' << {{label}} + + #content-body.section + -block content + + #footer.grid-container.section + .avatar + .user + .source + %a href='{{cfg.git_repo}}' target='_new' << {{cfg.name}}/{{cfg.version}} + + %script type='application/javascript' src='{{cfg.proto}}://{{cfg.web_host}}/framework/static/menu.js' diff --git a/http_server/izzylib/http_server/frontend/error.haml b/http_server/izzylib/http_server/frontend/error.haml new file mode 100644 index 0000000..5279a4f --- /dev/null +++ b/http_server/izzylib/http_server/frontend/error.haml @@ -0,0 +1,8 @@ +-extends 'base.haml' +-set page = 'Error' +-block content + %center + %font size='8' + HTTP {{response.status}} + %br + =error_message diff --git a/http_server/izzylib/http_server/frontend/static/icon512.png b/http_server/izzylib/http_server/frontend/static/icon512.png new file mode 100644 index 0000000..dce6a50 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/icon512.png differ diff --git a/http_server/izzylib/http_server/frontend/static/icon64.png b/http_server/izzylib/http_server/frontend/static/icon64.png new file mode 100644 index 0000000..1f5b602 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/icon64.png differ diff --git a/http_server/izzylib/http_server/frontend/static/menu.js b/http_server/izzylib/http_server/frontend/static/menu.js new file mode 100644 index 0000000..0f1aee0 --- /dev/null +++ b/http_server/izzylib/http_server/frontend/static/menu.js @@ -0,0 +1,29 @@ +const sidebarBox = document.querySelector('#menu'), + sidebarBtn = document.querySelector('#btn'), + pageWrapper = document.querySelector('html'); + header = document.querySelector('#header') + +sidebarBtn.addEventListener('click', event => { + sidebarBtn.classList.toggle('active'); + sidebarBox.classList.toggle('active'); +}); + +pageWrapper.addEventListener('click', event => { + itemId = event.srcElement.id + itemClass = event.srcElement.className + indexId = ['menu', 'btn', 'items'].indexOf(itemId) + indexClass = ['item', 'item name', 'items'].indexOf(itemClass) + + if (sidebarBox.classList.contains('active') && (indexId == -1 && indexClass == -1)) { + sidebarBtn.classList.remove('active'); + sidebarBox.classList.remove('active'); + } +}); + +window.addEventListener('keydown', event => { + + if (sidebarBox.classList.contains('active') && event.keyCode === 27) { + sidebarBtn.classList.remove('active'); + sidebarBox.classList.remove('active'); + } +}); diff --git a/http_server/izzylib/http_server/frontend/static/menu.svg b/http_server/izzylib/http_server/frontend/static/menu.svg new file mode 100644 index 0000000..aa7866e --- /dev/null +++ b/http_server/izzylib/http_server/frontend/static/menu.svg @@ -0,0 +1,84 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Black.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Black.ttf new file mode 100644 index 0000000..093fbf9 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Black.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Black.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Black.woff2 new file mode 100644 index 0000000..9b7309a Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Black.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BlackItalic.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BlackItalic.ttf new file mode 100644 index 0000000..00db009 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BlackItalic.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BlackItalic.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BlackItalic.woff2 new file mode 100644 index 0000000..7951598 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BlackItalic.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Bold.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Bold.ttf new file mode 100644 index 0000000..a3ca4b6 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Bold.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Bold.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Bold.woff2 new file mode 100644 index 0000000..a23fbda Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Bold.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BoldItalic.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BoldItalic.ttf new file mode 100644 index 0000000..1bccb97 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BoldItalic.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BoldItalic.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BoldItalic.woff2 new file mode 100644 index 0000000..43622e6 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-BoldItalic.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBold.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBold.ttf new file mode 100644 index 0000000..a732425 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBold.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBold.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBold.woff2 new file mode 100644 index 0000000..dd16c01 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBold.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBoldItalic.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000..05e26d6 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBoldItalic.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBoldItalic.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..cb8cc31 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraBoldItalic.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLight.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLight.ttf new file mode 100644 index 0000000..2ad4ac0 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLight.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLight.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLight.woff2 new file mode 100644 index 0000000..737badc Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLight.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLightItalic.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLightItalic.ttf new file mode 100644 index 0000000..b73b0fa Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLightItalic.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLightItalic.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLightItalic.woff2 new file mode 100644 index 0000000..53e80bb Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-ExtraLightItalic.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Italic.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Italic.ttf new file mode 100644 index 0000000..85269a1 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Italic.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Italic.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Italic.woff2 new file mode 100644 index 0000000..b052fc4 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Italic.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Light.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Light.ttf new file mode 100644 index 0000000..2f5d049 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Light.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Light.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Light.woff2 new file mode 100644 index 0000000..eb054ec Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Light.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-LightItalic.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-LightItalic.ttf new file mode 100644 index 0000000..bac17d0 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-LightItalic.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-LightItalic.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-LightItalic.woff2 new file mode 100644 index 0000000..de69781 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-LightItalic.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Regular.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Regular.ttf new file mode 100644 index 0000000..9abe932 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Regular.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Regular.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Regular.woff2 new file mode 100644 index 0000000..fbc66d6 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-Regular.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBold.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBold.ttf new file mode 100644 index 0000000..c27eaf4 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBold.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBold.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBold.woff2 new file mode 100644 index 0000000..21314cf Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBold.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBoldItalic.ttf b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBoldItalic.ttf new file mode 100644 index 0000000..59b6402 Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBoldItalic.ttf differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBoldItalic.woff2 b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBoldItalic.woff2 new file mode 100644 index 0000000..1abd05c Binary files /dev/null and b/http_server/izzylib/http_server/frontend/static/nunito/NunitoSans-SemiBoldItalic.woff2 differ diff --git a/http_server/izzylib/http_server/frontend/static/nunito/SIL Open Font License.txt b/http_server/izzylib/http_server/frontend/static/nunito/SIL Open Font License.txt new file mode 100644 index 0000000..3a7ebff --- /dev/null +++ b/http_server/izzylib/http_server/frontend/static/nunito/SIL Open Font License.txt @@ -0,0 +1,44 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/http_server/izzylib/http_server/middleware.py b/http_server/izzylib/http_server/middleware.py new file mode 100644 index 0000000..e241f96 --- /dev/null +++ b/http_server/izzylib/http_server/middleware.py @@ -0,0 +1,40 @@ +import multiprocessing + +from datetime import datetime, timedelta, timezone +from izzylib import logging + +from . import start_time + + +class MiddlewareBase: + attach = 'request' + + def __init__(self, app): + self.app = app + self.cfg = app.cfg + + + async def handler(self, request): + pass + + +class Headers(MiddlewareBase): + attach = 'response' + + async def handler(self, request, response): + if request.path.startswith('/framework') or request.path == '/favicon.ico': + max_age = int(timedelta(weeks=2).total_seconds()) + response.headers['Cache-Control'] = f'immutable,private,max-age={max_age}' + + response.headers['Server'] = f'{self.cfg.name}/{self.cfg.version}' + response.headers['Trans'] = 'Rights' + + +class AccessLog(MiddlewareBase): + attach = 'response' + + async def handler(self, request, response): + uagent = request.headers.get('user-agent', 'None') + address = request.headers.get('x-real-ip', request.forwarded.get('for', request.remote_addr)) + + logging.info(f'({multiprocessing.current_process().name}) {address} {request.method} {request.path} {response.status} "{uagent}"') diff --git a/http_server/izzylib/http_server/misc.py b/http_server/izzylib/http_server/misc.py new file mode 100644 index 0000000..b1f000c --- /dev/null +++ b/http_server/izzylib/http_server/misc.py @@ -0,0 +1,31 @@ +from izzylib import LowerDotDict + + +def ReplaceHeader(headers, key, value): + for k,v in headers.items(): + if k.lower() == header.lower(): + del headers[k] + + +class Headers(LowerDotDict): + def __init__(self, headers): + super().__init__() + + for k,v in headers.items(): + if not self.get(k): + self[k] = [] + + self[k].append(v) + + + def getone(self, key, default=None): + value = self.get(key) + + if not value: + return default + + return value[0] + + + def getall(self, key, default=[]): + return self.get(key.lower(), default) diff --git a/http_server/izzylib/http_server/request.py b/http_server/izzylib/http_server/request.py new file mode 100644 index 0000000..7923881 --- /dev/null +++ b/http_server/izzylib/http_server/request.py @@ -0,0 +1,103 @@ +import sanic + +from .misc import Headers + + +class Request(sanic.request.Request): + def __init__(self, url_bytes, headers, version, method, transport, app): + super().__init__(url_bytes, headers, version, method, transport, app) + + self.Headers = Headers(headers) + self.Data = Data(self) + self.template = self.app.template + self.setup() + + + def setup(self): + pass + + + def response(self, tpl, *args, **kwargs): + return self.template.response(self, tpl, *args, **kwargs) + + + def alldata(self): + return DotDict( + **self.content.json, + **self.data.query, + **self.data.form + ) + + + def json_check(self): + if self.path.endswith('.json'): + return True + + accept = self.headers.getone('Accept', None) + + if accept: + mimes = [v.strip() for v in accept.split(',')] + + if any(mime in ['application/json', 'application/activity+json'] for mime in mimes): + return True + + return False + + +class Data(object): + def __init__(self, request): + self.request = request + + + @property + def combined(self): + return DotDict(**self.form.asDict(), **self.query.asDict(), **self.json.asDict()) + + + @property + def query(self): + data = {k: v for k,v in parse_qsl(self.request.query_string)} + return DotDict(data) + + + @property + def form(self): + data = {k: v[0] for k,v in self.request.form.items()} + return DotDict(data) + + + @property + def files(self): + return DotDict({k:v[0] for k,v in self.request.files.items()}) + + + ### body functions + @property + def raw(self): + try: + return self.request.body + except Exception as e: + logging.verbose('IzzyLib.http_server.Data.raw: failed to get body') + logging.debug(f'{e.__class__.__name__}: {e}') + return b'' + + + @property + def text(self): + try: + return self.raw.decode() + except Exception as e: + logging.verbose('IzzyLib.http_server.Data.text: failed to get body') + logging.debug(f'{e.__class__.__name__}: {e}') + return '' + + + @property + def json(self): + try: + return DotDict(self.text) + except Exception as e: + logging.verbose('IzzyLib.http_server.Data.json: failed to get body') + logging.debug(f'{e.__class__.__name__}: {e}') + data = '{}' + return {} diff --git a/http_server/izzylib/http_server/response.py b/http_server/izzylib/http_server/response.py new file mode 100644 index 0000000..cce236b --- /dev/null +++ b/http_server/izzylib/http_server/response.py @@ -0,0 +1,211 @@ +import json, sanic + +from izzylib import DotDict +from izzylib.template import Color +from sanic.compat import Header +from sanic.cookies import CookieJar + + +class Response: + content_types = DotDict({ + 'text': 'text/plain', + 'html': 'text/html', + 'css': 'text/css', + 'javascript': 'application/javascript', + 'json': 'application/json', + 'activitypub': 'application/activity+json' + }) + + default_theme = DotDict({ + 'primary': Color('#e7a'), + 'secondary': Color('#a7e'), + 'background': Color('#191919'), + 'positive': Color('#aea'), + 'negative': Color('#e99'), + 'white': Color('#eee'), + 'black': Color('#111'), + 'speed': 250 + }) + + + def __init__(self, app, request, body=None, headers={}, cookies={}, status=200, content_type='text/html'): + # server objects + self.app = app + self.cfg = app.cfg + self.request = request + + # init vars + self._body = None + self._content_type = content_type + + self.headers = Header(headers) + self.cookies = CookieJar(self.headers) + self.body = body + self.status = status + + for cookie in cookies.items(): + pass + + + def __str__(self): + return self.body + + + def __bytes__(self): + return self.body.encode('utf-8') + + + def __repr__(self): + return self.get_response() + + + @property + def body(self): + return self._body + + + @body.setter + def body(self, body): + if not body: + self._body = b'' + return + + if self.content_type in [self.content_types.json, self.content_types.activitypub]: + body = json.dumps(body) + + if isinstance(body, str): + self._body = body.encode('utf-8') + + elif isinstance(body, bytes): + self._body = body + + else: + raise TypeError(f'Response body must be a string or bytes, not {body.__class__.__name__}') + + + @property + def content_type(self): + return self._content_type + + + @content_type.setter + def content_type(self, ctype): + self._content_type = self.content_types.get(ctype, ctype) + + + def set_headers(self, data: dict): + try: + self.set_content_type(headers.pop('content-type')) + + except: + pass + + self.headers.clear() + self.headers.update(data) + + + def set_cookie(self, key, value, data={}, **kwargs): + self.cookies[key] = value + + data.update(kwargs) + + for k,v in data.items(): + if k.lower() == 'max-age': + if isinstance(v, timedelta): + v = int(v.total_seconds()) + + elif not isinstance(v, int): + raise TypeError('Max-Age must be an integer or timedelta') + + elif k.lower() == 'expires': + if isinstance(v, datetime): + v = v.strftime('%a, %d-%b-%Y %T GMT') + + elif not isinstance(v, str): + raise TypeError('Expires must be a string or datetime') + + self.cookies[key][k] = v + + + def get_cookie(self, key): + try: + cookie = self.cookies[key] + except KeyError: + return None + + return + + + def del_cookie(self, key): + del self.cookies[key] + + + def template(self, tplfile, context={}, headers={}, status=200, content_type='text/html', cookies={}, pprint=False): + self.status = status + context.update({ + 'response': self, + **self.default_theme + }) + + html = self.app.template.render(tplfile, context, request=self, pprint=pprint) + return self.html(html, headers=headers, status=status, content_type=content_type, cookies=cookies) + + + def error(self, message, status=500, **kwargs): + if self.request and 'json' in self.request.headers.get('accept', ''): + return self.json({f'error {status}': message}, status=status, **kwargs) + + return self.template('error.haml', {'error_message': message}, status=status, **kwargs) + + + def json(self, body={}, headers={}, status=200, content_type='application/json', cookies={}): + body = json.dumps(body) + return self.get_response(body, headers, status, content_type, cookies) + + + def text(self, body, headers={}, status=200, content_type='text/plain', cookies={}): + return self.get_response(body, headers, status, content_type, cookies) + + + def html(self, *args, **kwargs): + self.content_type = 'text/html' + return self.text(*args, **kwargs) + + + def css(self, *args, **kwargs): + self.content_type = 'text/css' + return self.text.text(*args, **kwargs) + + + def javascript(self, *args, **kwargs): + self.content_type = 'application/javascript' + return self.text.text(*args, **kwargs) + + + def activitypub(self, *args, **kwargs): + self.content_type = 'application/activity+json' + return self.text.text(*args, **kwargs) + + + def redir(self, path, status=302, headers={}): + return sanic.response.redirect(path, status=status, headers={}) + + + def set_data(self, body=None, headers={}, status=200, content_type='text/html', cookies={}): + ctype = self.content_types.get(content_type, content_type) + + self.body = body + self.headers = headers + self.status = status + self.content_type = content_type + self.headers = headers + self.headers.pop('content-type', None) + + + def get_response(self, *args, **kwargs): + self.set_data(*args, **kwargs) + + response = sanic.response.HTTPResponse(self.body, self.status, self.headers, self.content_type) + response._cookies = self.cookies + + return response diff --git a/http_server/izzylib/http_server/view.py b/http_server/izzylib/http_server/view.py new file mode 100644 index 0000000..a561c5d --- /dev/null +++ b/http_server/izzylib/http_server/view.py @@ -0,0 +1,55 @@ +from izzylib.template import Color +from sanic.views import HTTPMethodView + +from .response import Response + + +class View(HTTPMethodView): + routes = [] + + + def dispatch_request(self, request, *args, **kwargs): + self.app = request.app + self.cfg = request.app.cfg + + handler = getattr(self, request.method.lower(), None) + return handler(request, Response(self.app, request), *args, **kwargs) + + +class Manifest(View): + paths = ['/framework/manifest.json'] + + + async def get(self, request, response): + data = { + 'name': self.cfg.name, + 'short_name': self.cfg.name.replace(' ', ''), + 'description': 'UvU', + 'icons': [ + { + 'src': "/framework/static/icon512.png", + 'sizes': '512x512', + 'type': 'image/png' + }, + { + 'src': "/framework/static/icon64.png", + 'sizes': '64x64', + 'type': 'image/png' + } + ], + 'theme_color': str(response.default_theme.primary), + 'background_color': str(response.default_theme.background), + 'display': 'standalone', + 'start_url': '/', + 'scope': f'{self.cfg.proto}://{self.cfg.web_host}' + } + + return response.json(data) + + +class Style(View): + paths = ['/framework/style.css'] + + async def get(self, request, response): + return response.template('base.css', content_type='text/css') + diff --git a/http_server/setup.py b/http_server/setup.py new file mode 100644 index 0000000..696621d --- /dev/null +++ b/http_server/setup.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +from setuptools import setup, find_namespace_packages + + +requires = [ + 'sanic==20.12.3', + 'sanic-cors==1.0.0', +] + + +setup( + name="IzzyLib HTTP Server", + version='0.6.0', + packages=find_namespace_packages(include=['izzylib.http_requests_client']), + python_requires='>=3.7.0', + install_requires=requires, + include_package_data=False, + author='Zoey Mae', + author_email='admin@barkshark.xyz', + description='An HTTP server based on Sanic', + keywords='web http server', + url='https://git.barkshark.xyz/izaliamae/izzylib', + project_urls={ + 'Bug Tracker': 'https://git.barkshark.xyz/izaliamae/izzylib/issues', + 'Documentation': 'https://git.barkshark.xyz/izaliamae/izzylib/wiki', + 'Source Code': 'https://git.barkshark.xyz/izaliamae/izzylib' + }, + + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Information Technology', + 'License :: Co-operative Non-violent Public License (CNPL 6+)', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] +) diff --git a/template/izzylib/template/__init__.py b/template/izzylib/template/__init__.py index 452c105..108c4b7 100644 --- a/template/izzylib/template/__init__.py +++ b/template/izzylib/template/__init__.py @@ -107,13 +107,13 @@ class Template(Environment): def update_filter(self, data): - if not isinstance(context, dict): + if not isinstance(data, dict): raise ValueError(f'Filter data not a dict') self.filters.update(data) - def render(self, tplfile, context={}, headers={}, cookies={}, request=None, pprint=False, **kwargs): + def render(self, tplfile, context={}, headers={}, cookies={}, request=None, pprint=False): if not isinstance(context, dict): raise TypeError(f'context for {tplfile} not a dict: {type(context)} {context}')