http_server_async: handle cookies
This commit is contained in:
parent
28a9bfb781
commit
e7d2b87a45
|
@ -1 +1 @@
|
||||||
recursive-include izzylib/http_server/frontend *
|
recursive-include izzylib/http_frontend *
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,25 +188,31 @@ 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)
|
||||||
|
|
||||||
|
try:
|
||||||
response.headers.update(self.cfg.default_headers)
|
response.headers.update(self.cfg.default_headers)
|
||||||
|
|
||||||
writer.write(response.compile())
|
writer.write(response.compile())
|
||||||
|
@ -186,6 +223,9 @@ class Application:
|
||||||
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):
|
||||||
for middleware in self.middleware['response' if response else 'request']:
|
for middleware in self.middleware['response' if response else 'request']:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue