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 .cookies import Cookies
from .middleware import Middleware
from .misc import Cookies, Headers
from .request import Request
from .response import Response
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 .response import Response
#from .router import Router
from .view import Static
from .. import logging
from ..dotdict import DotDict
from ..exceptions import MethodNotHandledException
from ..path import Path
from ..template import Template
frontend = Path(__file__).resolve().parent.parent.join('http_frontend')
class Application:
@ -23,10 +29,24 @@ class Application:
self.router = Router()
self.middleware = DotDict({'request': [], 'response': []})
#self.add_view = self.router.add_view
#self.add_route = self.router.add_route
#self.add_static = self.router.add_static
#self.get_route = self.router.get_route
self.template = Template(
self.cfg.tpl_search,
self.cfg.tpl_globals,
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.SIGINT, self.stop)
@ -56,6 +76,13 @@ class Application:
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):
if not asyncio.iscoroutinefunction(handler):
raise TypeError('Middleware handler must be a coroutine function or method')
@ -83,6 +110,10 @@ class Application:
mwlist.append(handler)
def render(self, template, *args, **kwargs):
return self.template.render(*args, **kwargs)
def stop(self, *_):
if not self._server:
print('server not running')
@ -157,34 +188,43 @@ class Application:
raise error.ServerError()
except NotFound:
response = self.cfg.response_class.error('Not Found', 404)
response = self.cfg.response_class()
response.error('Not Found', 404)
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:
response = self.cfg.response_class.error(e.message, e.status)
response = self.cfg.response_class()
response.error(e.message, e.status)
except:
traceback.print_exc()
response = self.cfg.response_class.error('Server Error', 500)
response = self.cfg.response_class()
response.error('Server Error', 500)
try:
await self.handle_middleware(request, response)
except:
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())
await writer.drain()
writer.close()
await writer.wait_closed()
writer.write(response.compile())
await writer.drain()
writer.close()
await writer.wait_closed()
if request:
logging.info(f'{request.remote} {request.method} {request.path} {response.status} {len(response.body)} {request.agent}')
if request:
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):

View file

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

View file

@ -2,23 +2,22 @@ import json
from datetime import datetime
from .cookies import Cookies
from .misc import Cookies, Headers
from ..dotdict import MultiDotDict
class Response:
__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 = body
self.headers = MultiDotDict(headers)
self.headers = Headers(headers)
self.cookies = Cookies(cookies)
self.status = status
if not self.headers.get('content-type'):
self.headers['content-type'] = 'text/plain'
self.body = body
self.status = status
self.content_type = content_type
@property
@ -28,59 +27,80 @@ class Response:
@body.setter
def body(self, data):
if isinstance(data, bytes):
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')
self._body = self._parse_body_data(data)
@classmethod
def text(cls, *args, **kwargs):
return cls(*args, **kwargs)
@property
def content_type(self):
return self.headers.getone('Content-Type')
@classmethod
def html(cls, *args, headers={}, **kwargs):
if not headers.get('content-type'):
headers['content-type'] = 'text/html'
return cls(*args, headers=headers, **kwargs)
@content_type.setter
def content_type(self, data):
self.headers['Content-Type'] = data
@classmethod
def json(cls, *args, headers={}, **kwargs):
if not headers.get('content-type'):
headers['content-type'] = 'application/json'
return cls(*args, headers=headers, **kwargs)
@property
def content_length(self):
return len(self.body)
@classmethod
def error(cls, message, status=500):
return cls(f'HTTP Error {status}: {message}', status=status)
def append(self, data):
self._body += self._parse_body_data(data)
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):
data = bytes(f'HTTP/1.1 {self.status}', 'utf-8')
for k,v in self.headers.items():
if k == 'Content-Length':
return
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'):
data += bytes(f'\r\nContent-Length: {len(self.body)}', 'utf-8')
for cookie in self.cookies.values():
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\nContent-Length: {len(self.body)}', 'utf-8')
data += b'\r\n\r\n'
data += self.body
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 (
InvalidMethodException,
MethodNotHandledException
@ -8,7 +11,6 @@ from ..exceptions import (
class View:
__path__ = ''
__slots__ = ['app']
def __init__(self, app):
self.app = app
@ -28,3 +30,25 @@ class View:
#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
envbash == 1.2.0
http_server_async =
http_router == 2.6.4
http-router == 2.6.4
python-magic == 0.4.24
http_signatures =
pycryptodome == 3.10.1
tldextract == 3.1.2