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 threading
import argparse import argparse
import RNS import RNS
from http.server import HTTPServer from http.server import HTTPServer, ThreadingHTTPServer
from db import init_db, get_setting, set_setting from db import init_db, get_setting, set_setting
from handlers import dispatch_request from handlers import dispatch_request
@ -101,7 +101,7 @@ def start_gateway(reticulum, bind_host="127.0.0.1"):
GatewayState.reticulum = reticulum GatewayState.reticulum = reticulum
GatewayState.local_dispatch = dispatch_request GatewayState.local_dispatch = dispatch_request
HTTPServer.allow_reuse_address = True 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 = threading.Thread(target=server.serve_forever, daemon=True)
thread.start() thread.start()

View file

@ -2,8 +2,9 @@ import re
import sys import sys
import time import time
import threading import threading
import collections
import RNS import RNS
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, ThreadingHTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
APP_NAME = "tinyweb" APP_NAME = "tinyweb"
@ -11,6 +12,10 @@ ASPECTS = ["server"]
GATEWAY_PORT = 8080 GATEWAY_PORT = 8080
REQUEST_TIMEOUT = 60 REQUEST_TIMEOUT = 60
MAX_BODY_SIZE = 16 * 1024 * 1024 # 16 MiB — covers /import and every other form 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: class GatewayState:
@ -67,12 +72,28 @@ def ensure_link():
class GatewayHandler(BaseHTTPRequestHandler): 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): def _forward(self, method):
parsed = urlparse(self.path) parsed = urlparse(self.path)
query = parse_qs(parsed.query) query = parse_qs(parsed.query)
body = {} body = {}
if method == "POST": if method == "POST":
if not self._check_rate_limit():
self.send_error(429, "Too many requests — slow down.")
return
try: try:
length = int(self.headers.get("Content-Length", 0)) length = int(self.headers.get("Content-Length", 0))
except ValueError: except ValueError:
@ -187,7 +208,7 @@ def main():
print(f"Gateway listening on http://localhost:{GATEWAY_PORT}") print(f"Gateway listening on http://localhost:{GATEWAY_PORT}")
print(f"Open http://localhost:{GATEWAY_PORT} in your browser") 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__": if __name__ == "__main__":

View file

@ -1043,9 +1043,10 @@ def handle_about():
return _respond( return _respond(
f'<h1>{esc(name)}</h1>' f'<h1>{esc(name)}</h1>'
f'<p>A personal search engine, built for the slow web.</p>' f'<p>A personal, decentralized search engine.</p>'
f'<p>TinyWeb is about taking back the internet. No algorithms, no ads, no tracking. ' f'<p>You save pages you find. They are stored locally and shared over a mesh network '
f'Just human-curated pages shared freely across a mesh network.</p>' 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'<ul>'
f'<li><b>{page_count}</b> page(s) indexed</li>' f'<li><b>{page_count}</b> page(s) indexed</li>'
f'<li><b>{tag_count}</b> tag(s)</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'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'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'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>' 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 { max-width: 500px; }
.forum-form input:not([type=checkbox]):not([type=radio]), .forum-form textarea { width: 100%; box-sizing: border-box; } .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 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 { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.forum-toolbar form { flex: 1; min-width: 160px; margin: 0; } .forum-toolbar form { flex: 1; min-width: 160px; margin: 0; }
.forum-toolbar input[name=q] { width: 100%; box-sizing: border-box; } .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 { margin: 0 0 0.8rem 0; }
.forum-status span { margin-right: 0.5rem; } .forum-status span { margin-right: 0.5rem; }
.forum-nav { margin: 0.5rem 0; } .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>""" </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>" 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-title { font-size: 1.05rem; margin-bottom: 0.1rem; }
.forum-list .thread-meta { font-size: 0.8rem; opacity: 0.7; } .forum-list .thread-meta { font-size: 0.8rem; opacity: 0.7; }
.forum-list .thread-badge { font-size: 0.78rem; opacity: 0.6; } .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 { 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; } .post-meta { font-size: 0.82rem; opacity: 0.7; }
.section { margin: 1.5rem 0; } .section { margin: 1.5rem 0; }

View file

@ -223,6 +223,21 @@
background: #f5f5f5; background: #f5f5f5;
} }
a.forum-action-inline { text-transform: none; font-size: 13px; padding: 2px 6px; border: none; } 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; } hr { border: none; border-top: 1px solid #eee; margin: 16px 0; }
small { small {