threaded HTTP server, rate limiting, remove slow-web rhetoric

This commit is contained in:
user 2026-06-09 01:07:36 +00:00
parent 54dbc1a3b1
commit 30f4f1ce3e
6 changed files with 59 additions and 19 deletions

4
app.py
View file

@ -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()

View file

@ -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__":

View file

@ -1043,9 +1043,10 @@ def handle_about():
return _respond(
f'<h1>{esc(name)}</h1>'
f'<p>A personal search engine, built for the slow web.</p>'
f'<p>TinyWeb is about taking back the internet. No algorithms, no ads, no tracking. '
f'Just human-curated pages shared freely across a mesh network.</p>'
f'<p>A personal, decentralized search engine.</p>'
f'<p>You save pages you find. They are stored locally and shared over a mesh network '
f'so other people can find them too.</p>'
f'<p>Search results come from your index and the indexes of people you are connected to.</p>'
f'<ul>'
f'<li><b>{page_count}</b> page(s) indexed</li>'
f'<li><b>{tag_count}</b> tag(s)</li>'
@ -1069,18 +1070,6 @@ def handle_about():
f'The <a href="/export">export</a> 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.</p>'
f'<h2>what is the slow web?</h2>'
f'<p>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.</p>'
f'<h2>how it works</h2>'
f'<ul>'
f'<li>Save pages you find valuable with the bookmarklet or /add</li>'
f'<li>Search your personal index — queries never leave your machine</li>'
f'<li>Subscribe to friends over Reticulum — encrypted, decentralized, works without the internet</li>'
f'<li>Tag and organize your collection into curated lists</li>'
f'</ul>'
f'<p><a href="/">search</a> | <a href="/pages">browse</a> | <a href="/tags">tags</a></p>'
)

View file

@ -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; }
</style>"""
DEFAULT_TEMPLATE = "<html>\n<head>\n<meta name=\"referrer\" content=\"no-referrer\">\n<meta http-equiv=\"x-dns-prefetch-control\" content=\"off\">\n" + FORUM_CSS + "</head>\n<body>\n{{content}}\n</body>\n</html>"

View file

@ -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; }

View file

@ -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 {