diff --git a/app.py b/app.py index 3830c85..c550760 100644 --- a/app.py +++ b/app.py @@ -4,7 +4,7 @@ import time import threading import argparse import RNS -from http.server import HTTPServer +from http.server import HTTPServer, ThreadingHTTPServer from db import init_db, get_setting, set_setting from handlers import dispatch_request @@ -101,7 +101,7 @@ def start_gateway(reticulum, bind_host="127.0.0.1"): GatewayState.reticulum = reticulum GatewayState.local_dispatch = dispatch_request HTTPServer.allow_reuse_address = True - server = HTTPServer((bind_host, gateway.GATEWAY_PORT), GatewayHandler) + server = ThreadingHTTPServer((bind_host, gateway.GATEWAY_PORT), GatewayHandler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() diff --git a/gateway.py b/gateway.py index d07924d..7d3fd53 100644 --- a/gateway.py +++ b/gateway.py @@ -2,8 +2,9 @@ import re import sys import time import threading +import collections import RNS -from http.server import HTTPServer, BaseHTTPRequestHandler +from http.server import HTTPServer, ThreadingHTTPServer, BaseHTTPRequestHandler from urllib.parse import parse_qs, urlparse APP_NAME = "tinyweb" @@ -11,6 +12,10 @@ ASPECTS = ["server"] GATEWAY_PORT = 8080 REQUEST_TIMEOUT = 60 MAX_BODY_SIZE = 16 * 1024 * 1024 # 16 MiB — covers /import and every other form +RATE_LIMIT_WINDOW = 60 +RATE_LIMIT_MAX = 30 +_rate_tracker = collections.defaultdict(list) +_rate_lock = threading.Lock() class GatewayState: @@ -67,12 +72,28 @@ def ensure_link(): class GatewayHandler(BaseHTTPRequestHandler): + def _check_rate_limit(self): + client = self.client_address[0] + now = time.time() + with _rate_lock: + times = _rate_tracker[client] + cutoff = now - RATE_LIMIT_WINDOW + while times and times[0] < cutoff: + times.pop(0) + if len(times) >= RATE_LIMIT_MAX: + return False + times.append(now) + return True + def _forward(self, method): parsed = urlparse(self.path) query = parse_qs(parsed.query) body = {} if method == "POST": + if not self._check_rate_limit(): + self.send_error(429, "Too many requests — slow down.") + return try: length = int(self.headers.get("Content-Length", 0)) except ValueError: @@ -187,7 +208,7 @@ def main(): print(f"Gateway listening on http://localhost:{GATEWAY_PORT}") print(f"Open http://localhost:{GATEWAY_PORT} in your browser") - HTTPServer(("127.0.0.1", GATEWAY_PORT), GatewayHandler).serve_forever() + ThreadingHTTPServer(("127.0.0.1", GATEWAY_PORT), GatewayHandler).serve_forever() if __name__ == "__main__": diff --git a/handlers.py b/handlers.py index 523000f..515e5cf 100644 --- a/handlers.py +++ b/handlers.py @@ -1043,9 +1043,10 @@ def handle_about(): return _respond( f'

{esc(name)}

' - f'

A personal search engine, built for the slow web.

' - f'

TinyWeb is about taking back the internet. No algorithms, no ads, no tracking. ' - f'Just human-curated pages shared freely across a mesh network.

' + f'

A personal, decentralized search engine.

' + f'

You save pages you find. They are stored locally and shared over a mesh network ' + f'so other people can find them too.

' + f'

Search results come from your index and the indexes of people you are connected to.

' f'