Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
#!/usr/bin/python3
|
|
"""Bounded disposable transfer endpoint, bound ONLY to the PeerTube Tailscale IP."""
|
|
import argparse
|
|
import http.server
|
|
import ipaddress
|
|
import json
|
|
import os
|
|
import socket
|
|
import threading
|
|
import time
|
|
import urllib.parse
|
|
|
|
MAX_BYTES = 16 * 1024**2
|
|
CHUNK = bytes(range(256)) * 4096
|
|
SLOTS = threading.BoundedSemaphore(2)
|
|
LOCK = threading.Lock()
|
|
RECENT = {}
|
|
|
|
|
|
class Probe(http.server.BaseHTTPRequestHandler):
|
|
protocol_version = 'HTTP/1.1'
|
|
|
|
def setup(self):
|
|
super().setup()
|
|
self.connection.settimeout(30)
|
|
|
|
def log_message(self, *_):
|
|
pass
|
|
|
|
def reply(self, code, data):
|
|
blob = json.dumps(data).encode()
|
|
self.send_response(code)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_header('Content-Length', str(len(blob)))
|
|
self.send_header('Cache-Control', 'no-store')
|
|
self.end_headers()
|
|
self.wfile.write(blob)
|
|
|
|
def admission(self, direction):
|
|
address = ipaddress.ip_address(self.client_address[0])
|
|
if address not in ipaddress.ip_network('100.64.0.0/10') and not address.is_loopback:
|
|
self.reply(403, {'error': 'Tailscale clients only'})
|
|
return False
|
|
with LOCK:
|
|
now = time.monotonic()
|
|
for key in list(RECENT):
|
|
if now - RECENT[key] > 60:
|
|
RECENT.pop(key)
|
|
key = (str(address), direction)
|
|
if now - RECENT.get(key, -60) < 10:
|
|
self.reply(429, {'error': 'probe rate limit'})
|
|
return False
|
|
RECENT[key] = now
|
|
if not SLOTS.acquire(blocking=False):
|
|
self.reply(503, {'error': 'probe busy'})
|
|
return False
|
|
return True
|
|
|
|
def do_GET(self):
|
|
parsed = urllib.parse.urlsplit(self.path)
|
|
if parsed.path == '/health':
|
|
return self.reply(200, {'ok': True, 'max_bytes': MAX_BYTES, 'storage': 'discarded'})
|
|
if parsed.path != '/download':
|
|
return self.reply(404, {'error': 'unknown endpoint'})
|
|
try:
|
|
count = int(urllib.parse.parse_qs(parsed.query).get('bytes', [8 * 1024**2])[0])
|
|
if not 65536 <= count <= MAX_BYTES:
|
|
raise ValueError()
|
|
except ValueError:
|
|
return self.reply(400, {'error': 'invalid size'})
|
|
if not self.admission('download'):
|
|
return
|
|
try:
|
|
self.send_response(200)
|
|
self.send_header('Content-Length', str(count))
|
|
self.send_header('Content-Type', 'application/octet-stream')
|
|
self.send_header('Cache-Control', 'no-store')
|
|
self.end_headers()
|
|
while count:
|
|
block = CHUNK[:min(count, len(CHUNK))]
|
|
self.wfile.write(block)
|
|
count -= len(block)
|
|
except (OSError, socket.timeout):
|
|
self.close_connection = True
|
|
finally:
|
|
SLOTS.release()
|
|
|
|
def do_PUT(self):
|
|
if self.path != '/upload':
|
|
self.close_connection = True
|
|
return self.reply(404, {'error': 'unknown endpoint'})
|
|
try:
|
|
count = int(self.headers.get('Content-Length', '0'))
|
|
if not 65536 <= count <= MAX_BYTES:
|
|
raise ValueError()
|
|
except ValueError:
|
|
self.close_connection = True
|
|
return self.reply(413, {'error': 'invalid size'})
|
|
if not self.admission('upload'):
|
|
self.close_connection = True
|
|
return
|
|
received = 0
|
|
try:
|
|
while received < count:
|
|
data = self.rfile.read(min(1024**2, count - received))
|
|
if not data:
|
|
raise OSError('short upload')
|
|
received += len(data)
|
|
self.reply(200, {'bytes_received': received})
|
|
except (OSError, socket.timeout):
|
|
self.close_connection = True
|
|
finally:
|
|
SLOTS.release()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--bind', required=True)
|
|
parser.add_argument('--port', type=int, default=9044)
|
|
args = parser.parse_args()
|
|
address = ipaddress.ip_address(args.bind)
|
|
if address not in ipaddress.ip_network('100.64.0.0/10') and not address.is_loopback:
|
|
raise SystemExit('Refusing a public/LAN/wildcard bind: require a Tailscale IP')
|
|
server = http.server.ThreadingHTTPServer((args.bind, args.port), Probe)
|
|
notify = os.environ.get('NOTIFY_SOCKET')
|
|
if notify:
|
|
address = '\0' + notify[1:] if notify.startswith('@') else notify
|
|
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as channel:
|
|
channel.connect(address)
|
|
channel.sendall(b'READY=1')
|
|
server.serve_forever()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|