Compare commits

...

9 Commits

Author SHA1 Message Date
0880
014cdd80e2 Update HTTPResponse major changes 2026-01-17 18:14:20 +03:30
0880
cc89a32eae Remove old CORS handling 2026-01-17 18:13:57 +03:30
0880
57b0d69eae Handle preflight 2026-01-17 18:13:48 +03:30
0880
ba149b2d47 Add PATCH and HEAD remove OPTIONS 2026-01-17 18:13:32 +03:30
0880
b35ab73daa add CORS settings 2026-01-17 18:13:09 +03:30
0880
1e610fbc9d Formatting 2026-01-17 18:13:01 +03:30
0880
46ebe53f71 Update request to include app 2026-01-17 18:12:51 +03:30
0880
569d400baf __contains__ method for Headers class 2026-01-17 18:12:34 +03:30
0880
83353522db Add CORS class 2026-01-17 18:12:18 +03:30

View File

@@ -9,6 +9,17 @@ from typing import Any, Awaitable, Callable, Optional
PR = re.compile(r"\<([a-zA-Z_][a-zA-Z0-9_]*)\>")
class CORS:
Origins: list[str]
Methods: list[str]
Disabled: bool
def __init__(self):
self.Disabled = False
self.Origins = []
self.Methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]
class Headers:
def __init__(self):
self._d: dict[str, str] = {}
@@ -22,13 +33,19 @@ class Headers:
def __str__(self):
return str(self._d)
def __contains__(self, key):
return self._d.__contains__(key)
class Request:
def __init__(self, method: str, path: str, headers: Headers, body: bytes):
def __init__(
self, method: str, path: str, headers: Headers, body: bytes, app: "App"
):
self.method = method
self.path = path
self.headers = headers
self.body = body
self.app = app
def __str__(self):
return str(
@@ -62,15 +79,14 @@ class App:
404: _default_404_route,
405: _default_405_route,
}
self.CORS = CORS()
def _pattern_to_regex(self, temp) -> re.Pattern[str]:
re_temp = temp
iter = PR.finditer(temp)
for m in iter:
name = m[1]
re_temp = re.sub(
m[0], r"(?P<" + name + r">[a-zA-Z0-9\-._~:%&=]+)", re_temp
)
re_temp = re.sub(m[0], r"(?P<" + name + r">[a-zA-Z0-9\-._~:%&=]+)", re_temp)
return re.compile(re_temp)
def _serve(
@@ -119,11 +135,20 @@ class App:
return decorator
def OPTIONS(self, path: str):
"""Decorator to register a OPTIONS HTTP route."""
def PATCH(self, path: str):
"""Decorator to register a PATCH HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "OPTIONS", func)
self._serve(path, "PATCH", func)
return func
return decorator
def HEAD(self, path: str):
"""Decorator to register a HEAD HTTP route."""
def decorator(func: Callable[[Request, ...], Awaitable[bytes]]):
self._serve(path, "HEAD", func)
return func
return decorator
@@ -179,20 +204,44 @@ class App:
content_length = int(headers.get("Content-Length", 0))
body = await reader.read(content_length) if content_length else b""
route, kwargs = self.resolve(path, method)
if method == "OPTIONS":
if "origin" in headers and headers.get("origin") in self.CORS.Origins:
origin = headers.get("origin")
response = "HTTP/1.1 200 OK\r\n"
response += "Content-Type: text/plain\r\n"
response += "Content-Length: 0\r\n"
response += f"Access-Control-Allow-Origin: {origin}\r\n"
response += f"Access-Control-Allow-Methods: {','.join(self.CORS.Methods)}\r\n"
response += "Access-Control-Allow-Headers: Content-Type,Authorization\r\n" # CORS
response += "Vary: Origin\r\n"
response += "\r\n"
response = await route(
request=Request(
method=method,
path=path,
headers=headers,
body=body,
),
**kwargs,
)
writer.write(response)
writer.write(response.encode(encoding="utf-8"))
await writer.drain()
await writer.drain()
else:
response = "HTTP/1.1 403 Forbidden\r\n"
response += "Content-Length: 0\r\n"
response += "Vary: Origin\r\n"
response += "\r\n"
writer.write(response.encode(encoding="utf-8"))
await writer.drain()
else:
route, kwargs = self.resolve(path, method)
response = await route(
request=Request(
method=method, path=path, headers=headers, body=body, app=self
),
**kwargs,
)
writer.write(response)
await writer.drain()
except Exception as e:
print(f"Internal Server Error: {e}")
finally:
@@ -209,37 +258,47 @@ class App:
await server.serve_forever()
AccessControlAllowOrigin = "*"
def HTTPResponse(
content: str, status=200, content_type="text/plain; charset=utf-8", headers=[]
request: Request,
content: str,
status=200,
content_type="text/plain; charset=utf-8",
headers=[],
) -> bytes:
global AccessControlAllowOrigin
head: str = f"HTTP/1.1 {status} {http.client.responses.get(status, 'Unkown Status Code')}\r\nContent-Type: {content_type}\r\n"
head += f"Access-Control-Allow-Origin: {AccessControlAllowOrigin}\r\n" # CORS
head += "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n" # CORS
content_bytes = content.encode(encoding="utf-8")
head: str = f"HTTP/1.1 {status} {http.client.responses.get(status, 'Unkown Status Code')}\r\nContent-Type: {content_type}\r\nContent-Length: {len(content_bytes)}\r\n"
if (
"origin" in request.headers
and not request.app.CORS.Disabled
and request.headers.get("origin") in request.app.CORS.Origins
):
head += (
f"Access-Control-Allow-Origin: {request.headers.get('origin')}\r\n" # CORS
)
head += f"Access-Control-Allow-Methods: {','.join(request.app.CORS.Methods)}\r\n" # CORS
head += "Access-Control-Allow-Headers: Content-Type,Authorization\r\n" # CORS
head += "Vary: Origin\r\n"
head += "\r\n".join(headers) + ("\r\n" if len(headers) > 0 else "")
head += "\r\n"
return (head + content).encode(encoding="utf-8")
return head.encode(encoding="utf-8") + content_bytes
_value_pattern = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*\}\}")
def render(file: str | Path, variables: dict[str, Any] = {}) -> bytes:
def render(request: Request, file: str | Path, variables: dict[str, Any] = {}) -> bytes:
if isinstance(file, str):
file = Path(file)
content: str = file.read_text(encoding="utf-8")
for m in _value_pattern.findall(content):
if m in variables:
content = re.sub(r"\{\{\s*" + m + r"\s*\}\}", variables[m], content)
return HTTPResponse(content, content_type="text/html; charset=utf-8")
return HTTPResponse(request, content, content_type="text/html; charset=utf-8")
def JSONResponse(d: dict, status=200) -> bytes:
def JSONResponse(request: Request, d: dict, status=200) -> bytes:
return HTTPResponse(
json.dumps(d), status=status, content_type="text/json; charset=utf-8"
request, json.dumps(d), status=status, content_type="text/json; charset=utf-8"
)
@@ -255,6 +314,6 @@ def JSONAPI(func):
):
return JSONResponse(result[1], result[0])
raise RuntimeError("Return value of JSONAPI route is not a dictionary")
return JSONResponse(result)
return JSONResponse(args[0], result)
return wrapper