'use strict'; // Transfer observation and canonical-origin routing. PeerTube retains queue selection and retries. const fs = require('node:fs'); const path = require('node:path'); const { AsyncLocalStorage } = require('node:async_hooks'); const context = new AsyncLocalStorage(); const state = process.env.PT_STATE_DIR || '/var/lib/pt-worker/state'; let route; try { const cfg = JSON.parse(fs.readFileSync(process.env.PT_WORKER_CONFIG || '/etc/pt-worker/worker.json')); if (cfg.canonical_origin && cfg.peertube_url) route = { from: new URL(cfg.canonical_origin), to: new URL(cfg.peertube_url) }; } catch { /* Enrollment and isolated tests may not yet have a config. */ } const originals = { 'http:': require('node:http').request, 'https:': require('node:https').request }; function emit(data) { try { fs.mkdirSync(state, { recursive: true, mode: 0o700 }); fs.appendFileSync(path.join(state, 'events.jsonl'), JSON.stringify({ time: Date.now() / 1000, ...data }) + '\n', { mode: 0o600 }); } catch { /* Observation never changes job behavior. */ } } const createWriteStream = fs.createWriteStream; fs.createWriteStream = function (filename, ...args) { const pathname = context.getStore(); const match = pathname?.match(/\/api\/v1\/runners\/jobs\/([a-f\d-]{36})\/files\//); if (match && typeof filename === 'string') { try { const dir = path.join(state, 'inputs'); fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); fs.writeFileSync(path.join(dir, path.basename(filename) + '.json'), JSON.stringify({ uuid: match[1] }), { mode: 0o600 }); } catch { /* Optional correlation only. */ } } return createWriteStream.call(this, filename, ...args); }; require('node:module').syncBuiltinESMExports(); for (const name of ['node:http', 'node:https']) { const mod = require(name); const original = mod.request; mod.request = function (...args) { let selected = original; // Source payloads use the public origin. Route only that origin via Tailscale. let options = args[0]; if (typeof options === 'string' || options instanceof URL) { const u = new URL(options); options = { protocol: u.protocol, hostname: u.hostname, port: u.port, path: u.pathname + u.search, ...(typeof args[1] === 'object' ? args[1] : {}) }; } if (route && (options.hostname || options.host) === route.from.hostname && (options.protocol || (name === 'node:https' ? 'https:' : 'http:')) === route.from.protocol && String(options.port || (route.from.protocol === 'https:' ? 443 : 80)) === String(route.from.port || (route.from.protocol === 'https:' ? 443 : 80))) { const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : undefined; options = { ...options, protocol: route.to.protocol, hostname: route.to.hostname, port: route.to.port, headers: { ...(options.headers || {}), host: route.from.host } }; delete options.host; selected = originals[route.to.protocol]; args = callback ? [options, callback] : [options]; } let req; if (typeof args[args.length - 1] === 'function') { const callback = args[args.length - 1]; args[args.length - 1] = function (...values) { return context.run((req.path || '').split('?')[0], () => callback.apply(this, values)); }; } req = selected.apply(this, args); // No host, URL, query, headers, body, or tokens are ever recorded. const pathname = (req.path || '').split('?')[0]; const match = pathname.match(/\/api\/v1\/runners\/jobs\/([a-f\d-]{36})\/(accept|success|error|abort)/); const download = pathname.includes('/files/'); const upload = (match && match[2] === 'success') || (req.method === 'PUT'); const begun = performance.now(); let sent = 0, received = 0, done = false, requestErrored = false; function size(value, encoding) { if (Buffer.isBuffer(value) || value instanceof Uint8Array) return value.length; if (typeof value === 'string') return Buffer.byteLength(value, typeof encoding === 'string' ? encoding : 'utf8'); return 0; } const write = req.write; req.write = function (chunk, ...rest) { sent += size(chunk, rest[0]); return write.call(this, chunk, ...rest); }; const end = req.end; req.end = function (chunk, ...rest) { sent += size(chunk, rest[0]); return end.call(this, chunk, ...rest); }; function finish(ok, code) { if (done) return; done = true; const seconds = Math.max(0.001, (performance.now() - begun) / 1000); if (download || upload) emit({ kind: 'transfer', direction: download ? 'download' : 'upload', bytes: download ? received : sent, seconds, ok, status_code: code || null }); if (match) emit({ kind: 'job_' + match[2], uuid: match[1], ok }); } req.on('response', res => { function interrupted() { finish(false, res.statusCode); req.destroy(); // A ClientRequest may already be marked destroyed when its response aborts; // destroy(error) then emits nothing. Forward one error to the upstream promise. queueMicrotask(() => { if (!requestErrored) req.emit('error', new Error('Worker transfer stream interrupted')); }); } res.on('data', chunk => { received += chunk.length; }); res.once('end', () => finish(res.statusCode >= 200 && res.statusCode < 300, res.statusCode)); res.once('aborted', interrupted); res.once('error', interrupted); }); req.on('error', () => { requestErrored = true; finish(false); }); return req; }; } require('node:module').syncBuiltinESMExports();