From 30f4f1ce3e105fc14f7c60dcba3fdad28125fedd Mon Sep 17 00:00:00 2001
From: user
Date: Tue, 9 Jun 2026 01:07:36 +0000
Subject: [PATCH] threaded HTTP server, rate limiting, remove slow-web rhetoric
---
app.py | 4 ++--
gateway.py | 25 +++++++++++++++++++++++--
handlers.py | 19 ++++---------------
templates.py | 12 ++++++++++++
themes/kodama2.html | 3 +++
themes/tinyweb-site.html | 15 +++++++++++++++
6 files changed, 59 insertions(+), 19 deletions(-)
diff --git a/app.py b/app.py
index addbcba..e621322 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 e30001b..2ddfe4a 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''
f'- {page_count} page(s) indexed
'
f'- {tag_count} tag(s)
'
@@ -1069,18 +1070,6 @@ def handle_about():
f'The export page gives you a JSON dump of pages only — '
f'it does not preserve your identity or subscription state, so it is a migration aid, '
f'not a substitute for a full backup.
'
- f'what is the slow web?
'
- f'The slow web is a movement for intentionality over speed, '
- f'human curation over algorithmic feeds, privacy over surveillance, '
- f'and community over corporations. Every page in this index was saved by a person '
- f'because they found it valuable — not because an algorithm told them to click.
'
- f'how it works
'
- f''
- f'- Save pages you find valuable with the bookmarklet or /add
'
- f'- Search your personal index — queries never leave your machine
'
- f'- Subscribe to friends over Reticulum — encrypted, decentralized, works without the internet
'
- f'- Tag and organize your collection into curated lists
'
- f'
'
f'search | browse | tags
'
)
diff --git a/templates.py b/templates.py
index c528b27..777ed63 100644
--- a/templates.py
+++ b/templates.py
@@ -12,6 +12,14 @@ FORUM_CSS = """
.forum-form { max-width: 500px; }
.forum-form input:not([type=checkbox]):not([type=radio]), .forum-form textarea { width: 100%; box-sizing: border-box; }
.forum-form textarea { resize: vertical; }
+.forum-form input, .forum-form textarea, .forum-form button { margin-bottom: 8px; }
+.forum-form small { display: block; margin-bottom: 6px; }
+.forum-form label { display: block; margin-bottom: 6px; }
+.forum-form + .forum-form { margin-top: 1rem; }
+.forum-form + .section-title { margin-top: 1rem; }
+.section-desc + .forum-form { margin-top: 0.8rem; }
+ul + .forum-form { margin-top: 1rem; }
+.checkbox-label { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.forum-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.forum-toolbar form { flex: 1; min-width: 160px; margin: 0; }
.forum-toolbar input[name=q] { width: 100%; box-sizing: border-box; }
@@ -20,6 +28,10 @@ FORUM_CSS = """
.forum-status { margin: 0 0 0.8rem 0; }
.forum-status span { margin-right: 0.5rem; }
.forum-nav { margin: 0.5rem 0; }
+.section { margin: 1.5rem 0; }
+.section-title { font-weight: 600; margin-bottom: 0.3rem; }
+.section-desc { font-size: 0.85rem; margin-bottom: 0.5rem; }
+.section ul { margin: 0.3rem 0; }
"""
DEFAULT_TEMPLATE = "\n\n\n\n" + FORUM_CSS + "\n\n{{content}}\n\n"
diff --git a/themes/kodama2.html b/themes/kodama2.html
index ebdfbfa..34fc125 100644
--- a/themes/kodama2.html
+++ b/themes/kodama2.html
@@ -348,6 +348,9 @@
.forum-list .thread-title { font-size: 1.05rem; margin-bottom: 0.1rem; }
.forum-list .thread-meta { font-size: 0.8rem; opacity: 0.7; }
.forum-list .thread-badge { font-size: 0.78rem; opacity: 0.6; }
+ .forum-form input, .forum-form textarea, .forum-form button { margin-bottom: 8px; }
+ .forum-form small { display: block; margin-bottom: 6px; }
+ .forum-form label { display: block; margin-bottom: 6px; }
.post { margin-bottom: 1rem; padding-left: 1rem; border-left: 1px solid rgba(40, 70, 65, 0.3); }
.post-meta { font-size: 0.82rem; opacity: 0.7; }
.section { margin: 1.5rem 0; }
diff --git a/themes/tinyweb-site.html b/themes/tinyweb-site.html
index f4d2f70..bda39ec 100644
--- a/themes/tinyweb-site.html
+++ b/themes/tinyweb-site.html
@@ -223,6 +223,21 @@
background: #f5f5f5;
}
a.forum-action-inline { text-transform: none; font-size: 13px; padding: 2px 6px; border: none; }
+ .section { margin: 1.5rem 0; }
+ .section-title { font-weight: 600; margin-bottom: 0.3rem; }
+ .section-desc { font-size: 0.85rem; color: #999; margin-bottom: 0.5rem; }
+ .section ul { margin: 0.3rem 0; }
+ .forum-form input, .forum-form textarea, .forum-form button { margin-bottom: 8px; }
+ .forum-form small { display: block; margin-bottom: 6px; }
+ .forum-form label { display: block; margin-bottom: 6px; }
+ .forum-form + .forum-form { margin-top: 1rem; }
+ .forum-form + .section-title { margin-top: 1rem; }
+ .section-desc + .forum-form { margin-top: 0.8rem; }
+ ul + .forum-form { margin-top: 1rem; }
+ .checkbox-label { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
+ .forum-status { font-size: 0.82rem; color: #999; margin: 0 0 0.8rem 0; }
+ .forum-status span { margin-right: 1.2rem; }
+ .forum-nav { margin: 1rem 0; }
hr { border: none; border-top: 1px solid #eee; margin: 16px 0; }
small {