first draft of http_server

This commit is contained in:
Izalia Mae 2021-08-17 13:49:19 -04:00
parent 62a2ab7115
commit bf8750a196
47 changed files with 1348 additions and 2 deletions

View file

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

View file

@ -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')

View file

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

View file

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

View file

@ -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;
}

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
%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'

View file

@ -0,0 +1,8 @@
-extends 'base.haml'
-set page = 'Error'
-block content
%center
%font size='8'
HTTP {{response.status}}
%br
=error_message

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -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');
}
});

View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="menu.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
id="svg8"
version="1.1"
viewBox="0 0 132.29167 79.375002"
height="300"
width="500">
<defs
id="defs2" />
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="36"
inkscape:window-x="36"
inkscape:window-height="990"
inkscape:window-width="1644"
units="px"
showgrid="true"
inkscape:document-rotation="0"
inkscape:current-layer="layer2"
inkscape:document-units="mm"
inkscape:cy="151.34478"
inkscape:cx="232.18877"
inkscape:zoom="1.4"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:snap-text-baseline="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-bbox="true"
inkscape:bbox-nodes="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
dotted="true"
id="grid1402"
type="xygrid"
originx="-7.9375001"
originy="-27.781234" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
id="layer2"
inkscape:groupmode="layer"
transform="translate(-7.9374999,-27.781233)">
<path
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.875,67.468765 c 116.41667,0 116.41667,0 116.41667,0 z"
id="path1590" />
<path
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.875,35.718766 c 116.41667,0 116.41667,0 116.41667,0 z"
id="path1590-7" />
<path
style="fill:none;fill-opacity:1;stroke:#cfcfcf;stroke-width:13.2292;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 15.875,99.218766 c 116.41667,0 116.41667,0 116.41667,0 z"
id="path1590-7-8" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

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

View file

@ -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}"')

View file

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

View file

@ -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 {}

View file

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

View file

@ -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')

43
http_server/setup.py Normal file
View file

@ -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'
]
)

View file

@ -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}')