586 lines
19 KiB
Python
586 lines
19 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
import asyncio
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import subprocess
|
||
|
|
import urllib.parse
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, List, Tuple, Optional
|
||
|
|
import pathlib
|
||
|
|
import ast
|
||
|
|
from types import SimpleNamespace
|
||
|
|
import time
|
||
|
|
from aiohttp import web
|
||
|
|
import aiohttp
|
||
|
|
|
||
|
|
# --- Optional external dependency used in your original code ---
|
||
|
|
from pr.ads import AsyncDataSet # noqa: E402
|
||
|
|
|
||
|
|
import sys
|
||
|
|
from http import cookies
|
||
|
|
import urllib.parse
|
||
|
|
import os
|
||
|
|
import io
|
||
|
|
import json
|
||
|
|
import urllib.request
|
||
|
|
import pathlib
|
||
|
|
import pickle
|
||
|
|
import zlib
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import time
|
||
|
|
from functools import wraps
|
||
|
|
from pathlib import Path
|
||
|
|
import pickle
|
||
|
|
import zlib
|
||
|
|
|
||
|
|
|
||
|
|
class Cache:
|
||
|
|
def __init__(self, base_dir: Path | str = "."):
|
||
|
|
self.base_dir = Path(base_dir).resolve()
|
||
|
|
self.base_dir.mkdir(exist_ok=True, parents=True)
|
||
|
|
|
||
|
|
def is_cacheable(self, obj):
|
||
|
|
return isinstance(obj, (int, str, bool, float))
|
||
|
|
|
||
|
|
def generate_key(self, *args, **kwargs):
|
||
|
|
|
||
|
|
return zlib.crc32(
|
||
|
|
json.dumps(
|
||
|
|
{
|
||
|
|
"args": [arg for arg in args if self.is_cacheable(arg)],
|
||
|
|
"kwargs": {k: v for k, v in kwargs.items() if self.is_cacheable(v)},
|
||
|
|
},
|
||
|
|
sort_keys=True,
|
||
|
|
default=str,
|
||
|
|
).encode()
|
||
|
|
)
|
||
|
|
|
||
|
|
def set(self, key, value):
|
||
|
|
key = self.generate_key(key)
|
||
|
|
data = {"value": value, "timestamp": time.time()}
|
||
|
|
serialized_data = pickle.dumps(data)
|
||
|
|
with open(self.base_dir.joinpath(f"{key}.cache"), "wb") as f:
|
||
|
|
f.write(serialized_data)
|
||
|
|
|
||
|
|
def get(self, key, default=None):
|
||
|
|
key = self.generate_key(key)
|
||
|
|
try:
|
||
|
|
with open(self.base_dir.joinpath(f"{key}.cache"), "rb") as f:
|
||
|
|
data = pickle.loads(f.read())
|
||
|
|
return data
|
||
|
|
except FileNotFoundError:
|
||
|
|
return default
|
||
|
|
|
||
|
|
def is_cacheable(self, obj):
|
||
|
|
return isinstance(obj, (int, str, bool, float))
|
||
|
|
|
||
|
|
def cached(self, expiration=60):
|
||
|
|
def decorator(func):
|
||
|
|
@wraps(func)
|
||
|
|
async def wrapper(*args, **kwargs):
|
||
|
|
# if not all(self.is_cacheable(arg) for arg in args) or not all(self.is_cacheable(v) for v in kwargs.values()):
|
||
|
|
# return await func(*args, **kwargs)
|
||
|
|
key = self.generate_key(*args, **kwargs)
|
||
|
|
print("Cache hit:", key)
|
||
|
|
cached_data = self.get(key)
|
||
|
|
if cached_data is not None:
|
||
|
|
if expiration is None or (time.time() - cached_data["timestamp"]) < expiration:
|
||
|
|
return cached_data["value"]
|
||
|
|
result = await func(*args, **kwargs)
|
||
|
|
self.set(key, result)
|
||
|
|
return result
|
||
|
|
|
||
|
|
return wrapper
|
||
|
|
|
||
|
|
return decorator
|
||
|
|
|
||
|
|
|
||
|
|
cache = Cache()
|
||
|
|
|
||
|
|
|
||
|
|
class CGI:
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
|
||
|
|
self.environ = os.environ
|
||
|
|
if not self.gateway_interface == "CGI/1.1":
|
||
|
|
return
|
||
|
|
self.status = 200
|
||
|
|
self.headers = {}
|
||
|
|
self.cache = Cache(pathlib.Path(__file__).parent.joinpath("cache/cgi"))
|
||
|
|
self.headers["Content-Length"] = "0"
|
||
|
|
self.headers["Cache-Control"] = "no-cache"
|
||
|
|
self.headers["Access-Control-Allow-Origin"] = "*"
|
||
|
|
self.headers["Content-Type"] = "application/json; charset=utf-8"
|
||
|
|
self.cookies = cookies.SimpleCookie()
|
||
|
|
|
||
|
|
self.query = urllib.parse.parse_qs(os.environ["QUERY_STRING"])
|
||
|
|
self.file = io.BytesIO()
|
||
|
|
|
||
|
|
def validate(self, val, fn, error):
|
||
|
|
if fn(val):
|
||
|
|
return True
|
||
|
|
self.status = 421
|
||
|
|
self.write({"type": "error", "message": error})
|
||
|
|
exit()
|
||
|
|
|
||
|
|
def __getitem__(self, key):
|
||
|
|
return self.get(key)
|
||
|
|
|
||
|
|
def get(self, key, default=None):
|
||
|
|
result = self.query.get(key, [default])
|
||
|
|
if len(result) == 1:
|
||
|
|
return result[0]
|
||
|
|
return result
|
||
|
|
|
||
|
|
def get_bool(self, key):
|
||
|
|
return str(self.get(key)).lower() in ["true", "yes", "1"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def cache_key(self):
|
||
|
|
return self.environ["CACHE_KEY"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def env(self):
|
||
|
|
env = os.environ.copy()
|
||
|
|
return env
|
||
|
|
|
||
|
|
@property
|
||
|
|
def gateway_interface(self):
|
||
|
|
return self.env.get("GATEWAY_INTERFACE", "")
|
||
|
|
|
||
|
|
@property
|
||
|
|
def request_method(self):
|
||
|
|
return self.env["REQUEST_METHOD"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def query_string(self):
|
||
|
|
return self.env["QUERY_STRING"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def script_name(self):
|
||
|
|
return self.env["SCRIPT_NAME"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def path_info(self):
|
||
|
|
return self.env["PATH_INFO"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def server_name(self):
|
||
|
|
return self.env["SERVER_NAME"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def server_port(self):
|
||
|
|
return self.env["SERVER_PORT"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def server_protocol(self):
|
||
|
|
return self.env["SERVER_PROTOCOL"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def remote_addr(self):
|
||
|
|
return self.env["REMOTE_ADDR"]
|
||
|
|
|
||
|
|
def validate_get(self, key):
|
||
|
|
self.validate(self.get(key), lambda x: bool(x), f"Missing {key}")
|
||
|
|
|
||
|
|
def print(self, data):
|
||
|
|
self.write(data)
|
||
|
|
|
||
|
|
def write(self, data):
|
||
|
|
if not isinstance(data, bytes) and not isinstance(data, str):
|
||
|
|
data = json.dumps(data, default=str, indent=4)
|
||
|
|
try:
|
||
|
|
data = data.encode()
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
self.file.write(data)
|
||
|
|
self.headers["Content-Length"] = str(len(self.file.getvalue()))
|
||
|
|
|
||
|
|
@property
|
||
|
|
def http_status(self):
|
||
|
|
return f"HTTP/1.1 {self.status}\r\n".encode("utf-8")
|
||
|
|
|
||
|
|
@property
|
||
|
|
def http_headers(self):
|
||
|
|
headers = io.BytesIO()
|
||
|
|
for header in self.headers:
|
||
|
|
headers.write(f"{header}: {self.headers[header]}\r\n".encode("utf-8"))
|
||
|
|
headers.write(b"\r\n")
|
||
|
|
return headers.getvalue()
|
||
|
|
|
||
|
|
@property
|
||
|
|
def http_body(self):
|
||
|
|
return self.file.getvalue()
|
||
|
|
|
||
|
|
@property
|
||
|
|
def http_response(self):
|
||
|
|
return self.http_status + self.http_headers + self.http_body
|
||
|
|
|
||
|
|
def flush(self, response=None):
|
||
|
|
|
||
|
|
if response:
|
||
|
|
try:
|
||
|
|
response = response.encode()
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
sys.stdout.buffer.write(response)
|
||
|
|
sys.stdout.buffer.flush()
|
||
|
|
return
|
||
|
|
sys.stdout.buffer.write(self.http_response)
|
||
|
|
sys.stdout.buffer.flush()
|
||
|
|
|
||
|
|
def __del__(self):
|
||
|
|
if self.http_body:
|
||
|
|
self.flush()
|
||
|
|
exit()
|
||
|
|
|
||
|
|
|
||
|
|
# -------------------------------
|
||
|
|
# Utilities
|
||
|
|
# -------------------------------
|
||
|
|
def get_function_source(name: str, directory: str = ".") -> List[Tuple[str, str]]:
|
||
|
|
matches: List[Tuple[str, str]] = []
|
||
|
|
for root, _, files in os.walk(directory):
|
||
|
|
for file in files:
|
||
|
|
if not file.endswith(".py"):
|
||
|
|
continue
|
||
|
|
path = os.path.join(root, file)
|
||
|
|
try:
|
||
|
|
with open(path, "r", encoding="utf-8") as fh:
|
||
|
|
source = fh.read()
|
||
|
|
tree = ast.parse(source, filename=path)
|
||
|
|
for node in ast.walk(tree):
|
||
|
|
if isinstance(node, ast.AsyncFunctionDef) and node.name == name:
|
||
|
|
func_src = ast.get_source_segment(source, node)
|
||
|
|
if func_src:
|
||
|
|
matches.append((path, func_src))
|
||
|
|
break
|
||
|
|
except (SyntaxError, UnicodeDecodeError):
|
||
|
|
continue
|
||
|
|
return matches
|
||
|
|
|
||
|
|
|
||
|
|
# -------------------------------
|
||
|
|
# Server
|
||
|
|
# -------------------------------
|
||
|
|
class Static(SimpleNamespace):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class View(web.View):
|
||
|
|
def __init__(self, *args, **kwargs):
|
||
|
|
super().__init__(*args, **kwargs)
|
||
|
|
self.db = self.request.app["db"]
|
||
|
|
self.server = self.request.app["server"]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def client(self):
|
||
|
|
return aiohttp.ClientSession()
|
||
|
|
|
||
|
|
|
||
|
|
class RetoorServer:
|
||
|
|
def __init__(self, base_dir: Path | str = ".", port: int = 8118) -> None:
|
||
|
|
self.base_dir = Path(base_dir).resolve()
|
||
|
|
self.port = port
|
||
|
|
|
||
|
|
self.static = Static()
|
||
|
|
self.db = AsyncDataSet(".default.db")
|
||
|
|
|
||
|
|
self._logger = logging.getLogger("retoor.server")
|
||
|
|
self._logger.setLevel(logging.INFO)
|
||
|
|
if not self._logger.handlers:
|
||
|
|
h = logging.StreamHandler(sys.stdout)
|
||
|
|
fmt = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
|
||
|
|
h.setFormatter(fmt)
|
||
|
|
self._logger.addHandler(h)
|
||
|
|
|
||
|
|
self._func_cache: Dict[str, Tuple[str, str]] = {}
|
||
|
|
self._compiled_cache: Dict[str, object] = {}
|
||
|
|
self._cgi_cache: Dict[str, Tuple[Optional[Path], Optional[str], Optional[str]]] = {}
|
||
|
|
self._static_path_cache: Dict[str, Path] = {}
|
||
|
|
self._base_dir_str = str(self.base_dir)
|
||
|
|
|
||
|
|
self.app = web.Application()
|
||
|
|
|
||
|
|
app = self.app
|
||
|
|
|
||
|
|
app["db"] = self.db
|
||
|
|
for path in pathlib.Path(__file__).parent.joinpath("web").joinpath("views").iterdir():
|
||
|
|
if path.suffix == ".py":
|
||
|
|
exec(open(path).read(), globals(), locals())
|
||
|
|
|
||
|
|
self.app["server"] = self
|
||
|
|
|
||
|
|
# from rpanel import create_app as create_rpanel_app
|
||
|
|
# self.app.add_subapp("/api/{tail:.*}", create_rpanel_app())
|
||
|
|
|
||
|
|
self.app.router.add_route("*", "/{tail:.*}", self.handle_any)
|
||
|
|
|
||
|
|
# ---------------------------
|
||
|
|
# Simple endpoints
|
||
|
|
# ---------------------------
|
||
|
|
async def handle_root(self, request: web.Request) -> web.Response:
|
||
|
|
return web.Response(text="Static file and cgi server from retoor.")
|
||
|
|
|
||
|
|
async def handle_hello(self, request: web.Request) -> web.Response:
|
||
|
|
return web.Response(text="Welcome to the custom HTTP server!")
|
||
|
|
|
||
|
|
# ---------------------------
|
||
|
|
# Dynamic function dispatch
|
||
|
|
# ---------------------------
|
||
|
|
def _path_to_funcname(self, path: str) -> str:
|
||
|
|
# /foo/bar -> foo_bar ; strip leading/trailing slashes safely
|
||
|
|
return path.strip("/").replace("/", "_")
|
||
|
|
|
||
|
|
async def _maybe_dispatch_dynamic(self, request: web.Request) -> Optional[web.StreamResponse]:
|
||
|
|
relpath = request.path.strip("/")
|
||
|
|
relpath = str(pathlib.Path(__file__).joinpath("cgi").joinpath(relpath).resolve()).replace(
|
||
|
|
"..", ""
|
||
|
|
)
|
||
|
|
if not relpath:
|
||
|
|
return None
|
||
|
|
|
||
|
|
last_part = relpath.split("/")[-1]
|
||
|
|
if "." in last_part:
|
||
|
|
return None
|
||
|
|
|
||
|
|
funcname = self._path_to_funcname(request.path)
|
||
|
|
|
||
|
|
if funcname not in self._compiled_cache:
|
||
|
|
entry = self._func_cache.get(funcname)
|
||
|
|
if entry is None:
|
||
|
|
matches = get_function_source(funcname, directory=str(self.base_dir))
|
||
|
|
if not matches:
|
||
|
|
return None
|
||
|
|
entry = matches[0]
|
||
|
|
self._func_cache[funcname] = entry
|
||
|
|
|
||
|
|
filepath, source = entry
|
||
|
|
code_obj = compile(source, filepath, "exec")
|
||
|
|
self._compiled_cache[funcname] = (code_obj, filepath)
|
||
|
|
|
||
|
|
code_obj, filepath = self._compiled_cache[funcname]
|
||
|
|
ctx = {
|
||
|
|
"static": self.static,
|
||
|
|
"request": request,
|
||
|
|
"relpath": relpath,
|
||
|
|
"os": os,
|
||
|
|
"sys": sys,
|
||
|
|
"web": web,
|
||
|
|
"asyncio": asyncio,
|
||
|
|
"subprocess": subprocess,
|
||
|
|
"urllib": urllib.parse,
|
||
|
|
"app": request.app,
|
||
|
|
"db": self.db,
|
||
|
|
}
|
||
|
|
exec(code_obj, ctx, ctx)
|
||
|
|
coro = ctx.get(funcname)
|
||
|
|
if not callable(coro):
|
||
|
|
return web.Response(status=500, text=f"Dynamic handler '{funcname}' is not callable.")
|
||
|
|
return await coro(request)
|
||
|
|
|
||
|
|
# ---------------------------
|
||
|
|
# Static files
|
||
|
|
# ---------------------------
|
||
|
|
async def handle_static(self, request: web.Request) -> web.StreamResponse:
|
||
|
|
path = request.path
|
||
|
|
|
||
|
|
if path in self._static_path_cache:
|
||
|
|
cached_path = self._static_path_cache[path]
|
||
|
|
if cached_path is None:
|
||
|
|
return web.Response(status=404, text="Not found")
|
||
|
|
try:
|
||
|
|
return web.FileResponse(path=cached_path)
|
||
|
|
except Exception as e:
|
||
|
|
return web.Response(status=500, text=f"Failed to send file: {e!r}")
|
||
|
|
|
||
|
|
relpath = path.replace('..",', "").lstrip("/").rstrip("/")
|
||
|
|
abspath = (self.base_dir / relpath).resolve()
|
||
|
|
|
||
|
|
if not str(abspath).startswith(self._base_dir_str):
|
||
|
|
return web.Response(status=403, text="Forbidden")
|
||
|
|
|
||
|
|
if not abspath.exists():
|
||
|
|
self._static_path_cache[path] = None
|
||
|
|
return web.Response(status=404, text="Not found")
|
||
|
|
|
||
|
|
if abspath.is_dir():
|
||
|
|
return web.Response(status=403, text="Directory listing forbidden")
|
||
|
|
|
||
|
|
self._static_path_cache[path] = abspath
|
||
|
|
try:
|
||
|
|
return web.FileResponse(path=abspath)
|
||
|
|
except Exception as e:
|
||
|
|
return web.Response(status=500, text=f"Failed to send file: {e!r}")
|
||
|
|
|
||
|
|
# ---------------------------
|
||
|
|
# CGI
|
||
|
|
# ---------------------------
|
||
|
|
def _find_cgi_script(
|
||
|
|
self, path_only: str
|
||
|
|
) -> Tuple[Optional[Path], Optional[str], Optional[str]]:
|
||
|
|
path_only = "pr/cgi/" + path_only
|
||
|
|
if path_only in self._cgi_cache:
|
||
|
|
return self._cgi_cache[path_only]
|
||
|
|
|
||
|
|
split_path = [p for p in path_only.split("/") if p]
|
||
|
|
for i in range(len(split_path), -1, -1):
|
||
|
|
candidate = "/" + "/".join(split_path[:i])
|
||
|
|
candidate_fs = (self.base_dir / candidate.lstrip("/")).resolve()
|
||
|
|
if (
|
||
|
|
str(candidate_fs).startswith(self._base_dir_str)
|
||
|
|
and candidate_fs.parent.is_dir()
|
||
|
|
and candidate_fs.parent.name == "cgi"
|
||
|
|
and candidate_fs.suffix == ".bin"
|
||
|
|
and candidate_fs.is_file()
|
||
|
|
and os.access(candidate_fs, os.X_OK)
|
||
|
|
):
|
||
|
|
script_name = candidate if candidate.startswith("/") else "/" + candidate
|
||
|
|
path_info = path_only[len(script_name) :]
|
||
|
|
result = (candidate_fs, script_name, path_info)
|
||
|
|
self._cgi_cache[path_only] = result
|
||
|
|
return result
|
||
|
|
|
||
|
|
result = (None, None, None)
|
||
|
|
self._cgi_cache[path_only] = result
|
||
|
|
return result
|
||
|
|
|
||
|
|
async def _run_cgi(
|
||
|
|
self, request: web.Request, script_path: Path, script_name: str, path_info: str
|
||
|
|
) -> web.Response:
|
||
|
|
start_time = time.time()
|
||
|
|
method = request.method
|
||
|
|
query = request.query_string
|
||
|
|
|
||
|
|
env = os.environ.copy()
|
||
|
|
env["CACHE_KEY"] = json.dumps(
|
||
|
|
{"method": method, "query": query, "script_name": script_name, "path_info": path_info},
|
||
|
|
default=str,
|
||
|
|
)
|
||
|
|
env["GATEWAY_INTERFACE"] = "CGI/1.1"
|
||
|
|
env["REQUEST_METHOD"] = method
|
||
|
|
env["QUERY_STRING"] = query
|
||
|
|
env["SCRIPT_NAME"] = script_name
|
||
|
|
env["PATH_INFO"] = path_info
|
||
|
|
|
||
|
|
host_parts = request.host.split(":", 1)
|
||
|
|
env["SERVER_NAME"] = host_parts[0]
|
||
|
|
env["SERVER_PORT"] = host_parts[1] if len(host_parts) > 1 else "80"
|
||
|
|
env["SERVER_PROTOCOL"] = "HTTP/1.1"
|
||
|
|
|
||
|
|
peername = request.transport.get_extra_info("peername")
|
||
|
|
env["REMOTE_ADDR"] = request.headers.get("HOST_X_FORWARDED_FOR", peername[0])
|
||
|
|
|
||
|
|
for hk, hv in request.headers.items():
|
||
|
|
hk_lower = hk.lower()
|
||
|
|
if hk_lower == "content-type":
|
||
|
|
env["CONTENT_TYPE"] = hv
|
||
|
|
elif hk_lower == "content-length":
|
||
|
|
env["CONTENT_LENGTH"] = hv
|
||
|
|
else:
|
||
|
|
env["HTTP_" + hk.upper().replace("-", "_")] = hv
|
||
|
|
|
||
|
|
post_data = None
|
||
|
|
if method in {"POST", "PUT", "PATCH"}:
|
||
|
|
post_data = await request.read()
|
||
|
|
env.setdefault("CONTENT_LENGTH", str(len(post_data)))
|
||
|
|
|
||
|
|
try:
|
||
|
|
proc = await asyncio.create_subprocess_exec(
|
||
|
|
str(script_path),
|
||
|
|
stdin=asyncio.subprocess.PIPE if post_data else None,
|
||
|
|
stdout=asyncio.subprocess.PIPE,
|
||
|
|
stderr=asyncio.subprocess.PIPE,
|
||
|
|
env=env,
|
||
|
|
)
|
||
|
|
stdout, stderr = await proc.communicate(input=post_data)
|
||
|
|
|
||
|
|
if proc.returncode != 0:
|
||
|
|
msg = stderr.decode(errors="ignore")
|
||
|
|
return web.Response(status=500, text=f"CGI script error:\n{msg}")
|
||
|
|
|
||
|
|
header_end = stdout.find(b"\r\n\r\n")
|
||
|
|
if header_end != -1:
|
||
|
|
offset = 4
|
||
|
|
else:
|
||
|
|
header_end = stdout.find(b"\n\n")
|
||
|
|
offset = 2
|
||
|
|
|
||
|
|
if header_end != -1:
|
||
|
|
body = stdout[header_end + offset :]
|
||
|
|
headers_blob = stdout[:header_end].decode(errors="ignore")
|
||
|
|
|
||
|
|
status_code = 200
|
||
|
|
headers: Dict[str, str] = {}
|
||
|
|
for line in headers_blob.splitlines():
|
||
|
|
line = line.strip()
|
||
|
|
if not line or ":" not in line:
|
||
|
|
continue
|
||
|
|
|
||
|
|
k, v = line.split(":", 1)
|
||
|
|
k_stripped = k.strip()
|
||
|
|
if k_stripped.lower() == "status":
|
||
|
|
try:
|
||
|
|
status_code = int(v.strip().split()[0])
|
||
|
|
except Exception:
|
||
|
|
status_code = 200
|
||
|
|
else:
|
||
|
|
headers[k_stripped] = v.strip()
|
||
|
|
|
||
|
|
end_time = time.time()
|
||
|
|
headers["X-Powered-By"] = "retoor"
|
||
|
|
headers["X-Date"] = time.strftime("%a, %d %b %Y %H:%M:%S %Z", time.localtime())
|
||
|
|
headers["X-Time"] = str(end_time)
|
||
|
|
headers["X-Duration"] = str(end_time - start_time)
|
||
|
|
if "error" in str(body).lower():
|
||
|
|
print(headers)
|
||
|
|
print(body)
|
||
|
|
return web.Response(body=body, headers=headers, status=status_code)
|
||
|
|
else:
|
||
|
|
return web.Response(body=stdout)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
return web.Response(status=500, text=f"Failed to execute CGI script.\n{e!r}")
|
||
|
|
|
||
|
|
# ---------------------------
|
||
|
|
# Main router
|
||
|
|
# ---------------------------
|
||
|
|
async def handle_any(self, request: web.Request) -> web.StreamResponse:
|
||
|
|
path = request.path
|
||
|
|
|
||
|
|
if path == "/":
|
||
|
|
return await self.handle_root(request)
|
||
|
|
if path == "/hello":
|
||
|
|
return await self.handle_hello(request)
|
||
|
|
|
||
|
|
script_path, script_name, path_info = self._find_cgi_script(path)
|
||
|
|
if script_path:
|
||
|
|
return await self._run_cgi(request, script_path, script_name or "", path_info or "")
|
||
|
|
|
||
|
|
dyn = await self._maybe_dispatch_dynamic(request)
|
||
|
|
if dyn is not None:
|
||
|
|
return dyn
|
||
|
|
|
||
|
|
return await self.handle_static(request)
|
||
|
|
|
||
|
|
# ---------------------------
|
||
|
|
# Runner
|
||
|
|
# ---------------------------
|
||
|
|
def run(self) -> None:
|
||
|
|
self._logger.info("Serving at port %d (base_dir=%s)", self.port, self.base_dir)
|
||
|
|
web.run_app(self.app, port=self.port)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
server = RetoorServer(base_dir=".", port=8118)
|
||
|
|
server.run()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|
||
|
|
else:
|
||
|
|
cgi = CGI()
|