#!/bin/sh # ═══════════════════════════════════════════════════════════════════════════════ # WebTorrent Controller - Browser Torrent Streaming # LXC container with Debian rootfs (no Docker/Podman) # ═══════════════════════════════════════════════════════════════════════════════ CONTAINER_NAME="webtorrent" CONTAINER_DIR="/srv/lxc/$CONTAINER_NAME" DATA_DIR="/srv/webtorrent" DOWNLOAD_DIR="/srv/downloads/webtorrent" CONFIG="webtorrent" # Debian LXC rootfs URL (arm64) DEBIAN_LXC_MIRROR="https://images.linuxcontainers.org/images/debian/bookworm/arm64/default" # Logging log_info() { logger -t webtorrent -p user.info "$*"; echo "[INFO] $*"; } log_error() { logger -t webtorrent -p user.error "$*"; echo "[ERROR] $*" >&2; } log_ok() { echo "[OK] $*"; } # UCI helpers uci_get() { uci -q get "$CONFIG.$1"; } # ───────────────────────────────────────────────────────────────────────────────── # Install container with Debian rootfs # ───────────────────────────────────────────────────────────────────────────────── cmd_install() { log_info "Installing WebTorrent container (Debian LXC)..." # Create directories mkdir -p "$CONTAINER_DIR/rootfs" mkdir -p "$DATA_DIR/config" mkdir -p "$DOWNLOAD_DIR" local rootfs="$CONTAINER_DIR/rootfs" # Download Debian LXC rootfs if not exists if [ ! -f "$rootfs/bin/bash" ]; then log_info "Downloading Debian LXC rootfs..." # Get latest build date from LXC mirror local latest=$(curl -sL "$DEBIAN_LXC_MIRROR/" | grep -oE '[0-9]{8}_[0-9]{2}:[0-9]{2}' | sort -r | head -1) [ -z "$latest" ] && latest="20240301_05:24" local rootfs_url="${DEBIAN_LXC_MIRROR}/${latest}/rootfs.tar.xz" local tarball="/tmp/debian-rootfs-webtorrent.tar.xz" log_info "Fetching: $rootfs_url" curl -L -o "$tarball" "$rootfs_url" || { log_error "Failed to download Debian rootfs" return 1 } log_info "Extracting rootfs..." xz -d -c "$tarball" | tar -xf - -C "$rootfs" || { log_error "Failed to extract rootfs" rm -f "$tarball" return 1 } rm -f "$tarball" fi # Configure DNS echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf" echo "nameserver 1.1.1.1" >> "$rootfs/etc/resolv.conf" # Create startup script cat > "$rootfs/start-webtorrent.sh" <<'STARTEOF' #!/bin/bash export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" export HOME="/root" exec > /config/startup.log 2>&1 echo "=== Starting WebTorrent $(date) ===" # Install Node.js and WebTorrent if not present if ! command -v node >/dev/null 2>&1; then echo "Installing Node.js..." apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ nodejs \ npm \ ca-certificates || { echo "apt install failed"; exit 1; } apt-get clean rm -rf /var/lib/apt/lists/* fi # Install webtorrent packages - use EXACT version 1.9.7 (last CommonJS version) # v2.x is ESM-only and breaks require() if [ ! -d "/opt/webtorrent/node_modules/webtorrent" ]; then echo "Installing WebTorrent packages (v1.9.7 for CommonJS)..." mkdir -p /opt/webtorrent cd /opt/webtorrent npm install webtorrent@1.9.7 express@4.18.0 cors@2.8.5 --save-exact --production || { echo "npm install failed"; exit 1; } fi # Create/update server script cat > /opt/webtorrent/server.js << 'SRVEOF' const WebTorrent = require('webtorrent'); const express = require('express'); const cors = require('cors'); const app = express(); const client = new WebTorrent(); const PORT = 8095; const DOWNLOAD_PATH = '/downloads'; app.use(cors()); app.use(express.json()); app.use('/downloads', express.static(DOWNLOAD_PATH)); app.get('/', (req, res) => { res.send(getHTML()); }); app.get('/api/torrents', (req, res) => { const torrents = client.torrents.map(t => ({ infoHash: t.infoHash, name: t.name, length: t.length, downloaded: t.downloaded, downloadSpeed: t.downloadSpeed, progress: t.progress, numPeers: t.numPeers, files: t.files.map(f => ({ name: f.name, path: f.path, length: f.length })) })); res.json(torrents); }); app.post('/api/add', (req, res) => { const { magnet } = req.body; if (!magnet) return res.status(400).json({ error: 'Magnet required' }); client.add(magnet, { path: DOWNLOAD_PATH }, torrent => { console.log('Added:', torrent.name); }); res.json({ success: true }); }); app.get('/stream/:infoHash/*', (req, res) => { const torrent = client.get(req.params.infoHash); if (!torrent) return res.status(404).send('Torrent not found'); const filePath = req.params[0]; const file = torrent.files.find(f => f.path === filePath); if (!file) return res.status(404).send('File not found'); const range = req.headers.range; if (range) { const positions = range.replace(/bytes=/, '').split('-'); const start = parseInt(positions[0], 10); const end = positions[1] ? parseInt(positions[1], 10) : file.length - 1; res.writeHead(206, { 'Content-Range': 'bytes ' + start + '-' + end + '/' + file.length, 'Accept-Ranges': 'bytes', 'Content-Length': (end - start) + 1, 'Content-Type': 'video/mp4' }); file.createReadStream({ start, end }).pipe(res); } else { res.writeHead(200, { 'Content-Length': file.length, 'Content-Type': 'video/mp4' }); file.createReadStream().pipe(res); } }); function getHTML() { return 'WebTorrent

WebTorrent Streaming

'; } app.listen(PORT, '0.0.0.0', () => console.log('WebTorrent on port ' + PORT)); SRVEOF echo "=== Starting WebTorrent server ===" mkdir -p /config /downloads cd /opt/webtorrent exec node server.js STARTEOF chmod +x "$rootfs/start-webtorrent.sh" # Create LXC config local memory=$(uci_get main.memory) [ -z "$memory" ] && memory="268435456" # 256MB in bytes cat > "$CONTAINER_DIR/config" </dev/null | grep -q "RUNNING"; then log_info "WebTorrent already running" return 0 fi log_info "Starting WebTorrent..." lxc-start -n "$CONTAINER_NAME" -f "$CONTAINER_DIR/config" # Wait for startup sleep 5 if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then log_ok "WebTorrent started on http://192.168.255.43:8095/" else log_error "Failed to start WebTorrent" lxc-info -n "$CONTAINER_NAME" 2>&1 return 1 fi } # ───────────────────────────────────────────────────────────────────────────────── # Stop container # ───────────────────────────────────────────────────────────────────────────────── cmd_stop() { if ! lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then log_info "WebTorrent not running" return 0 fi log_info "Stopping WebTorrent..." lxc-stop -n "$CONTAINER_NAME" -t 30 log_ok "WebTorrent stopped" } # ───────────────────────────────────────────────────────────────────────────────── # Restart container # ───────────────────────────────────────────────────────────────────────────────── cmd_restart() { cmd_stop sleep 2 cmd_start } # ───────────────────────────────────────────────────────────────────────────────── # Status # ───────────────────────────────────────────────────────────────────────────────── cmd_status() { echo "=== WebTorrent Status ===" # Container state if lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then echo "Container: RUNNING" echo "IP: 192.168.255.43" else echo "Container: STOPPED" return 0 fi # API status local torrents=$(curl -s "http://192.168.255.43:8095/api/torrents" 2>/dev/null) if [ -n "$torrents" ]; then local count=$(echo "$torrents" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) echo "Active Torrents: ${count:-0}" fi echo "Web UI: http://192.168.255.43:8095/" } # ───────────────────────────────────────────────────────────────────────────────── # Logs # ───────────────────────────────────────────────────────────────────────────────── cmd_logs() { local lines="${1:-50}" if [ -f "$DATA_DIR/config/startup.log" ]; then tail -n "$lines" "$DATA_DIR/config/startup.log" else log_info "No logs yet." fi } # ───────────────────────────────────────────────────────────────────────────────── # Shell access # ───────────────────────────────────────────────────────────────────────────────── cmd_shell() { if ! lxc-info -n "$CONTAINER_NAME" 2>/dev/null | grep -q "RUNNING"; then log_error "Container not running" return 1 fi lxc-attach -n "$CONTAINER_NAME" -- /bin/bash } # ───────────────────────────────────────────────────────────────────────────────── # Add torrent # ───────────────────────────────────────────────────────────────────────────────── cmd_add() { local magnet="$1" [ -z "$magnet" ] && { log_error "Usage: webtorrentctl add "; return 1; } local result=$(curl -s -X POST "http://192.168.255.43:8095/api/add" \ -H "Content-Type: application/json" \ -d "{\"magnet\":\"$magnet\"}" 2>/dev/null) if echo "$result" | grep -q "success"; then log_ok "Torrent added" else log_error "Failed to add torrent" fi } # ───────────────────────────────────────────────────────────────────────────────── # List torrents # ───────────────────────────────────────────────────────────────────────────────── cmd_list() { local torrents=$(curl -s "http://192.168.255.43:8095/api/torrents" 2>/dev/null) if [ -n "$torrents" ]; then echo "$torrents" | python3 -c " import sys, json try: data = json.load(sys.stdin) if not data: print('No torrents') for i, t in enumerate(data, 1): name = t.get('name', 'N/A')[:50] progress = t.get('progress', 0) * 100 peers = t.get('numPeers', 0) print(f'{i}. {name} ({progress:.1f}%) - {peers} peers') except: pass " 2>/dev/null else log_info "No torrents or server not running" fi } # ───────────────────────────────────────────────────────────────────────────────── # Configure HAProxy exposure # ───────────────────────────────────────────────────────────────────────────────── cmd_configure_haproxy() { local domain=$(uci_get exposure.domain) [ -z "$domain" ] && domain="stream.gk2.secubox.in" log_info "Configuring HAProxy for $domain" # Create backend uci set haproxy.webtorrent_web=backend uci set haproxy.webtorrent_web.name='webtorrent_web' uci set haproxy.webtorrent_web.mode='http' uci set haproxy.webtorrent_web.server="webtorrent 192.168.255.43:8095 weight 100 check" # Create vhost local vhost_id=$(echo "$domain" | tr '.' '_') uci set "haproxy.$vhost_id=vhost" uci set "haproxy.$vhost_id.domain=$domain" uci set "haproxy.$vhost_id.backend=mitmproxy_inspector" uci set "haproxy.$vhost_id.original_backend=webtorrent_web" uci set "haproxy.$vhost_id.ssl=1" uci set "haproxy.$vhost_id.ssl_redirect=1" uci set "haproxy.$vhost_id.acme=1" uci commit haproxy # Add mitmproxy route if [ -f /srv/mitmproxy/haproxy-routes.json ]; then python3 -c " import json with open('/srv/mitmproxy/haproxy-routes.json') as f: routes = json.load(f) routes['$domain'] = ['192.168.255.43', 8095] with open('/srv/mitmproxy/haproxy-routes.json', 'w') as f: json.dump(routes, f, indent=2) " 2>/dev/null fi # Reload haproxyctl reload 2>/dev/null || true /etc/init.d/mitmproxy restart 2>/dev/null || true log_ok "HAProxy configured: https://$domain/" } # ───────────────────────────────────────────────────────────────────────────────── # Uninstall # ───────────────────────────────────────────────────────────────────────────────── cmd_uninstall() { log_info "Uninstalling WebTorrent..." cmd_stop 2>/dev/null rm -rf "$CONTAINER_DIR" log_info "Container removed. Data preserved in $DATA_DIR" log_ok "WebTorrent uninstalled" } # ───────────────────────────────────────────────────────────────────────────────── # Main # ───────────────────────────────────────────────────────────────────────────────── case "$1" in install) cmd_install ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart ;; status) cmd_status ;; logs) shift; cmd_logs "$@" ;; shell) cmd_shell ;; add) shift; cmd_add "$@" ;; list) cmd_list ;; configure-haproxy) cmd_configure_haproxy ;; uninstall) cmd_uninstall ;; *) echo "WebTorrent Controller - Browser Torrent Streaming" echo "" echo "Usage: webtorrentctl " echo "" echo "Commands:" echo " install Install Debian LXC container with WebTorrent" echo " start Start WebTorrent" echo " stop Stop WebTorrent" echo " restart Restart WebTorrent" echo " status Show status" echo " logs [n] Show last n log lines (default 50)" echo " shell Interactive shell in container" echo " add Add torrent by magnet link" echo " list List active torrents" echo " configure-haproxy Setup HAProxy reverse proxy" echo " uninstall Remove container (keeps data)" ;; esac