diff --git a/MANIFEST.in b/MANIFEST.in index 8b1396a..002157c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -recursive-include izzylib/http_server/frontend * +recursive-include izzylib/http_frontend * diff --git a/izzylib/http_server_async/__init__.py b/izzylib/http_server_async/__init__.py index 35d13d1..b5c5577 100644 --- a/izzylib/http_server_async/__init__.py +++ b/izzylib/http_server_async/__init__.py @@ -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 diff --git a/izzylib/http_server_async/application.py b/izzylib/http_server_async/application.py index e4eb70e..1c7cb26 100644 --- a/izzylib/http_server_async/application.py +++ b/izzylib/http_server_async/application.py @@ -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): diff --git a/izzylib/http_server_async/request.py b/izzylib/http_server_async/request.py index 1cadc83..dac9646 100644 --- a/izzylib/http_server_async/request.py +++ b/izzylib/http_server_async/request.py @@ -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 diff --git a/izzylib/http_server_async/response.py b/izzylib/http_server_async/response.py index 89cb624..7e3c57a 100644 --- a/izzylib/http_server_async/response.py +++ b/izzylib/http_server_async/response.py @@ -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 diff --git a/izzylib/http_server_async/view.py b/izzylib/http_server_async/view.py index a1a8610..e47ee80 100644 --- a/izzylib/http_server_async/view.py +++ b/izzylib/http_server_async/view.py @@ -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 diff --git a/setup.cfg b/setup.cfg index 49e65fd..5329962 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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