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 .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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue