threaded HTTP server, rate limiting, remove slow-web rhetoric
This commit is contained in:
parent
0473fe2d42
commit
17afc541bb
6 changed files with 59 additions and 19 deletions
4
app.py
4
app.py
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
25
gateway.py
25
gateway.py
|
|
@ -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__":
|
||||||
|
|
|
||||||
19
handlers.py
19
handlers.py
|
|
@ -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>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
12
templates.py
12
templates.py
|
|
@ -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>"
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue