http_server_async: handle cookies

This commit is contained in:
Izalia Mae 2021-10-24 12:58:02 -04:00
parent 28a9bfb781
commit e7d2b87a45
7 changed files with 187 additions and 80 deletions

View file

@ -1 +1 @@
recursive-include izzylib/http_server/frontend * recursive-include izzylib/http_frontend *

View file

@ -2,9 +2,9 @@ http_methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST',
from .application import Application from .application import Application
from .cookies import Cookies
from .middleware import Middleware from .middleware import Middleware
from .misc import Cookies, Headers
from .request import Request from .request import Request
from .response import Response from .response import Response
from .router import Router from .router import Router
from .view import View from .view import View, Static

View file

@ -7,10 +7,16 @@ from . import http_methods, error
from .config import Config from .config import Config
from .response import Response from .response import Response
#from .router import Router #from .router import Router
from .view import Static
from .. import logging from .. import logging
from ..dotdict import DotDict from ..dotdict import DotDict
from ..exceptions import MethodNotHandledException from ..exceptions import MethodNotHandledException
from ..path import Path
from ..template import Template
frontend = Path(__file__).resolve().parent.parent.join('http_frontend')
class Application: class Application:
@ -23,10 +29,24 @@ class Application:
self.router = Router() self.router = Router()
self.middleware = DotDict({'request': [], 'response': []}) self.middleware = DotDict({'request': [], 'response': []})
#self.add_view = self.router.add_view self.template = Template(
#self.add_route = self.router.add_route self.cfg.tpl_search,
#self.add_static = self.router.add_static self.cfg.tpl_globals,
#self.get_route = self.router.get_route self.cfg.tpl_context,
self.cfg.tpl_autoescape
)
self.template.add_env('app', self)
self.template.add_env('cfg', self.cfg)
self.template.add_env('len', len)
if self.cfg.tpl_default:
self.template.add_search_path(frontend)
#self.add_view(view.Manifest)
#self.add_view(view.Robots)
#self.add_view(view.Style)
self.add_static('/framework/favicon.ico', frontend.join('static/icon64.png'))
self.add_static('/framework/static/', frontend.join('static'))
signal.signal(signal.SIGHUP, self.stop) signal.signal(signal.SIGHUP, self.stop)
signal.signal(signal.SIGINT, self.stop) signal.signal(signal.SIGINT, self.stop)
@ -56,6 +76,13 @@ class Application:
pass pass
def add_static(self, path, src):
if Path(src).isdir:
path = Path(path).join('{path}')
self.add_route(Static(src), path)
def add_middleware(self, handler): def add_middleware(self, handler):
if not asyncio.iscoroutinefunction(handler): if not asyncio.iscoroutinefunction(handler):
raise TypeError('Middleware handler must be a coroutine function or method') raise TypeError('Middleware handler must be a coroutine function or method')
@ -83,6 +110,10 @@ class Application:
mwlist.append(handler) mwlist.append(handler)
def render(self, template, *args, **kwargs):
return self.template.render(*args, **kwargs)
def stop(self, *_): def stop(self, *_):
if not self._server: if not self._server:
print('server not running') print('server not running')
@ -157,34 +188,43 @@ class Application:
raise error.ServerError() raise error.ServerError()
except NotFound: except NotFound:
response = self.cfg.response_class.error('Not Found', 404) response = self.cfg.response_class()
response.error('Not Found', 404)
except MethodNotAllowed: except MethodNotAllowed:
response = self.cfg.response_class.error('Method Not Allowed', 405) response = self.cfg.response_class()
response.error('Method Not Allowed', 405)
except error.HttpError as e: except error.HttpError as e:
response = self.cfg.response_class.error(e.message, e.status) response = self.cfg.response_class()
response.error(e.message, e.status)
except: except:
traceback.print_exc() traceback.print_exc()
response = self.cfg.response_class.error('Server Error', 500) response = self.cfg.response_class()
response.error('Server Error', 500)
try: try:
await self.handle_middleware(request, response) await self.handle_middleware(request, response)
except: except:
traceback.print_exc() traceback.print_exc()
response = Response.error('Server Error', 500) response = Response()
response.error('Server Error', 500)
response.headers.update(self.cfg.default_headers) try:
response.headers.update(self.cfg.default_headers)
writer.write(response.compile()) writer.write(response.compile())
await writer.drain() await writer.drain()
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
if request: if request:
logging.info(f'{request.remote} {request.method} {request.path} {response.status} {len(response.body)} {request.agent}') logging.info(f'{request.remote} {request.method} {request.path} {response.status} {len(response.body)} {request.agent}')
except:
traceback.print_exc()
async def handle_middleware(self, request, response=None): async def handle_middleware(self, request, response=None):

View file

@ -3,6 +3,8 @@ import asyncio, email, traceback
from datetime import datetime, timezone from datetime import datetime, timezone
from urllib.parse import unquote_plus from urllib.parse import unquote_plus
from .misc import Cookies, Headers, CookieItem
from ..dotdict import DotDict, MultiDotDict from ..dotdict import DotDict, MultiDotDict
@ -11,7 +13,9 @@ LocalTime = datetime.now(UtcTime).astimezone().tzinfo
class Request: class Request:
__slots__ = ['_body', '_form', '_reader', 'app', 'address', 'method', 'path', 'version', 'headers', 'query', 'raw_query', 'ctx'] __slots__ = ['_body', '_form', '_reader', 'app', 'address', 'method', 'path', 'version', 'headers', 'cookies', 'query', 'raw_query']
ctx = DotDict()
def __init__(self, app, reader, address): def __init__(self, app, reader, address):
super().__init__() super().__init__()
@ -19,15 +23,16 @@ class Request:
self._body = b'' self._body = b''
self._form = DotDict() self._form = DotDict()
self.headers = Headers()
self.cookies = Cookies()
self.query = DotDict()
self.app = app self.app = app
self.address = address self.address = address
self.method = None self.method = None
self.path = None self.path = None
self.version = None self.version = None
self.headers = MultiDotDict()
self.query = DotDict()
self.raw_query = None self.raw_query = None
self.ctx = DotDict()
def __getitem__(self, key): def __getitem__(self, key):
@ -38,31 +43,35 @@ class Request:
self.ctx[key] = value self.ctx[key] = value
def __getattr__(self, key): ## These two functions are a mess and break everything
return self.ctx[key] #def __getattr__(self, key):
#if key in self.__slots__:
#return super().__getattr__(self, key)
#return self.ctx[key]
def __setattr__(self, key, value): #def __setattr__(self, key, value):
if key not in self.__slots__: #if key not in self.__slots__:
self.ctx[key] = value #self.ctx[key] = value
else: #else:
super().__setattr__(key, value) #super().__setattr__(key, value)
@property @property
def agent(self): def agent(self):
return self.headers.get('User-Agent', 'no agent') return self.headers.getone('User-Agent', 'no agent')
@property @property
def content_type(self): def content_type(self):
return self.headers.get('Content-Type', '') return self.headers.getone('Content-Type', '')
@property @property
def date(self): def date(self):
date_str = self.headers.get('Date') date_str = self.headers.getone('Date')
if date_str: if date_str:
date = datetime.strptime(date_str, '%a, %d %b %Y %H:%M:%S GMT') date = datetime.strptime(date_str, '%a, %d %b %Y %H:%M:%S GMT')
@ -75,17 +84,17 @@ class Request:
@property @property
def host(self): def host(self):
return self.headers.get('Host') return self.headers.getone('Host')
@property @property
def length(self): def length(self):
return int(self.headers.get('Content-Length', 0)) return int(self.headers.getone('Content-Length', 0))
@property @property
def remote(self): def remote(self):
return self.headers.get('X-Real-Ip', self.headers.get('X-Forwarded-For', self.address)) return self.headers.getone('X-Real-Ip', self.headers.getone('X-Forwarded-For', self.address))
async def read(self, length=2048, timeout=None): async def read(self, length=2048, timeout=None):
@ -137,9 +146,22 @@ class Request:
self.query[key] = value self.query[key] = value
else: else:
try: key, value = line.split(': ') try: key, value = line.split(': ', 1)
except: continue except: continue
if key.lower() == 'cookie':
for cookie in value.split(';'):
try:
item = CookieItem.from_string(cookie)
except:
traceback.print_exc()
continue
self.cookies[item.key] = item
continue
self.headers[key] = value self.headers[key] = value

View file

@ -2,23 +2,22 @@ import json
from datetime import datetime from datetime import datetime
from .cookies import Cookies from .misc import Cookies, Headers
from ..dotdict import MultiDotDict from ..dotdict import MultiDotDict
class Response: class Response:
__slots__ = ['_body', 'headers', 'cookies', 'status'] __slots__ = ['_body', 'headers', 'cookies', 'status']
def __init__(self, body=b'', status=200, headers={}, cookies={}): def __init__(self, body=b'', status=200, headers={}, cookies={}, content_type='text/plain'):
self._body = b'' self._body = b''
self.body = body self.headers = Headers(headers)
self.headers = MultiDotDict(headers)
self.cookies = Cookies(cookies) self.cookies = Cookies(cookies)
self.status = status
if not self.headers.get('content-type'): self.body = body
self.headers['content-type'] = 'text/plain' self.status = status
self.content_type = content_type
@property @property
@ -28,59 +27,80 @@ class Response:
@body.setter @body.setter
def body(self, data): def body(self, data):
if isinstance(data, bytes): self._body = self._parse_body_data(data)
self._body += data
elif isinstance(data, str):
self._body += data.encode('utf-8')
elif any(map(isinstance, [data], [dict, list, tuple])):
self._body += json.dumps(data).encode('utf-8')
else:
self._body += str(data).encode('utf-8')
@classmethod @property
def text(cls, *args, **kwargs): def content_type(self):
return cls(*args, **kwargs) return self.headers.getone('Content-Type')
@classmethod @content_type.setter
def html(cls, *args, headers={}, **kwargs): def content_type(self, data):
if not headers.get('content-type'): self.headers['Content-Type'] = data
headers['content-type'] = 'text/html'
return cls(*args, headers=headers, **kwargs)
@classmethod @property
def json(cls, *args, headers={}, **kwargs): def content_length(self):
if not headers.get('content-type'): return len(self.body)
headers['content-type'] = 'application/json'
return cls(*args, headers=headers, **kwargs)
@classmethod def append(self, data):
def error(cls, message, status=500): self._body += self._parse_body_data(data)
return cls(f'HTTP Error {status}: {message}', status=status)
def set_html(self, body=b''):
self.content_type = 'text/html'
self.body = body
def json(self, body={}, activity=False):
self.content_type = 'application/activity+json' if activity else 'application/json'
self.body = body
def error(self, message, status=500):
self.body = f'HTTP Error {status}: {message}'
self.status = status
def delete_cookie(self, cookie):
cookie.set_delete()
self.cookies[cookie.key] = cookie
def compile(self): def compile(self):
data = bytes(f'HTTP/1.1 {self.status}', 'utf-8') data = bytes(f'HTTP/1.1 {self.status}', 'utf-8')
for k,v in self.headers.items(): for k,v in self.headers.items():
if k == 'Content-Length':
return
for value in v: for value in v:
data += bytes(f'\r\n{k.capitalize()}: {value}', 'utf-8') data += bytes(f'\r\n{k}: {value}', 'utf-8')
if not self.headers.get('content-length'): for cookie in self.cookies.values():
data += bytes(f'\r\nContent-Length: {len(self.body)}', 'utf-8') data += bytes(f'\r\nSet-Cookie: {cookie}', 'utf-8')
if not self.headers.get('date'): if not self.headers.get('Date'):
data += bytes(f'\r\nDate: {datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")}', 'utf-8') data += bytes(f'\r\nDate: {datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")}', 'utf-8')
data += bytes(f'\r\nContent-Length: {len(self.body)}', 'utf-8')
data += b'\r\n\r\n' data += b'\r\n\r\n'
data += self.body data += self.body
return data return data
def _parse_body_data(self, data):
if isinstance(data, str):
data = data.encode('utf-8')
elif any(map(isinstance, [data], [dict, list, tuple])):
data = json.dumps(data).encode('utf-8')
else:
data = str(data).encode('utf-8')
return data

View file

@ -1,5 +1,8 @@
from . import http_methods import magic
from . import http_methods, error
from ..path import Path
from ..exceptions import ( from ..exceptions import (
InvalidMethodException, InvalidMethodException,
MethodNotHandledException MethodNotHandledException
@ -8,7 +11,6 @@ from ..exceptions import (
class View: class View:
__path__ = '' __path__ = ''
__slots__ = ['app']
def __init__(self, app): def __init__(self, app):
self.app = app self.app = app
@ -28,3 +30,25 @@ class View:
#pass #pass
def Static(src):
src = Path(src)
async def StaticHandler(request, path=None):
try:
if not path:
src_path = src
else:
src_path = src.join(path)
with open(src_path, 'rb') as fd:
data = fd.read()
mime = magic.from_buffer(data[:2048], mime=True)
except (FileNotFoundError, IsADirectoryError) as e:
raise error.NotFound('Static file not found')
headers = {}
return dict(body=data, content_type=mime)
return StaticHandler

View file

@ -47,7 +47,8 @@ http_server =
sanic == 21.6.2 sanic == 21.6.2
envbash == 1.2.0 envbash == 1.2.0
http_server_async = http_server_async =
http_router == 2.6.4 http-router == 2.6.4
python-magic == 0.4.24
http_signatures = http_signatures =
pycryptodome == 3.10.1 pycryptodome == 3.10.1
tldextract == 3.1.2 tldextract == 3.1.2