|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +import fs from 'fs' |
| 4 | +import path from 'path' |
| 5 | +import minimist from 'minimist' |
| 6 | +import Client from 'bittorrent-tracker' |
| 7 | +import { generateKeypair } from './lib/crypto.js' |
| 8 | +import { createManifest, validateManifest } from './lib/manifest.js' |
| 9 | + |
| 10 | +const argv = minimist(process.argv.slice(2), { |
| 11 | + alias: { |
| 12 | + k: 'keyfile', |
| 13 | + t: 'tracker', |
| 14 | + i: 'input', |
| 15 | + o: 'output' |
| 16 | + }, |
| 17 | + default: { |
| 18 | + keyfile: './identity.json', |
| 19 | + tracker: 'ws://localhost:8000' // Default to local WS tracker |
| 20 | + } |
| 21 | +}) |
| 22 | + |
| 23 | +const command = argv._[0] |
| 24 | + |
| 25 | +if (!command) { |
| 26 | + console.error(`Usage: |
| 27 | + gen-key [-k identity.json] |
| 28 | + publish [-k identity.json] [-t ws://tracker] -i magnet_list.txt |
| 29 | + subscribe [-t ws://tracker] <public_key_hex> |
| 30 | + `) |
| 31 | + process.exit(1) |
| 32 | +} |
| 33 | + |
| 34 | +// 1. Generate Key |
| 35 | +if (command === 'gen-key') { |
| 36 | + const keypair = generateKeypair() |
| 37 | + const data = { |
| 38 | + publicKey: keypair.publicKey.toString('hex'), |
| 39 | + secretKey: keypair.secretKey.toString('hex') |
| 40 | + } |
| 41 | + fs.writeFileSync(argv.keyfile, JSON.stringify(data, null, 2)) |
| 42 | + console.log(`Identity generated at ${argv.keyfile}`) |
| 43 | + console.log(`Public Key: ${data.publicKey}`) |
| 44 | + process.exit(0) |
| 45 | +} |
| 46 | + |
| 47 | +// 2. Publish |
| 48 | +if (command === 'publish') { |
| 49 | + if (!fs.existsSync(argv.keyfile)) { |
| 50 | + console.error('Keyfile not found. Run gen-key first.') |
| 51 | + process.exit(1) |
| 52 | + } |
| 53 | + const keyData = JSON.parse(fs.readFileSync(argv.keyfile)) |
| 54 | + const keypair = { |
| 55 | + publicKey: Buffer.from(keyData.publicKey, 'hex'), |
| 56 | + secretKey: Buffer.from(keyData.secretKey, 'hex') |
| 57 | + } |
| 58 | + |
| 59 | + // Read magnet links |
| 60 | + if (!argv.input) { |
| 61 | + console.error('Please specify input file with -i (list of magnet links)') |
| 62 | + process.exit(1) |
| 63 | + } |
| 64 | + const content = fs.readFileSync(argv.input, 'utf-8') |
| 65 | + const lines = content.split('\n').map(l => l.trim()).filter(l => l.length > 0) |
| 66 | + |
| 67 | + // Create Collections structure (flat for now) |
| 68 | + const collections = [{ |
| 69 | + title: 'Default Collection', |
| 70 | + items: lines |
| 71 | + }] |
| 72 | + |
| 73 | + // Create Manifest |
| 74 | + // TODO: Retrieve last sequence from somewhere or store it? |
| 75 | + // For now, we use timestamp as sequence to be simple and monotonic |
| 76 | + const sequence = Date.now() |
| 77 | + const manifest = createManifest(keypair, sequence, collections) |
| 78 | + |
| 79 | + console.log('Publishing manifest:', JSON.stringify(manifest, null, 2)) |
| 80 | + |
| 81 | + // Connect to Tracker |
| 82 | + const client = new Client({ |
| 83 | + infoHash: Buffer.alloc(20), // Dummy, not used for custom proto |
| 84 | + peerId: Buffer.alloc(20), // Dummy |
| 85 | + announce: [argv.tracker], |
| 86 | + port: 6666 |
| 87 | + }) |
| 88 | + |
| 89 | + client.on('error', err => console.error('Client Error:', err.message)) |
| 90 | + |
| 91 | + client.on('update', () => { |
| 92 | + // We don't expect standard updates |
| 93 | + }) |
| 94 | + |
| 95 | + // We need to access the underlying socket to send our custom message |
| 96 | + // bittorrent-tracker abstracts this, so we might need to hook into the `announce` phase or just use the socket directly if exposed. |
| 97 | + // The library exposes `client._trackers` which is a list of Tracker instances. |
| 98 | + |
| 99 | + // Wait for socket connection |
| 100 | + setTimeout(() => { |
| 101 | + const trackers = client._trackers |
| 102 | + let sent = false |
| 103 | + |
| 104 | + for (const tracker of trackers) { |
| 105 | + // We only support WebSocket for this custom protocol right now |
| 106 | + if (tracker.socket && tracker.socket.readyState === 1) { // OPEN |
| 107 | + console.log('Sending publish message to ' + tracker.announceUrl) |
| 108 | + tracker.socket.send(JSON.stringify({ |
| 109 | + action: 'publish', |
| 110 | + manifest |
| 111 | + })) |
| 112 | + sent = true |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + if (sent) console.log('Publish sent!') |
| 117 | + else console.error('No connected websocket trackers found.') |
| 118 | + |
| 119 | + setTimeout(() => { |
| 120 | + client.destroy() |
| 121 | + process.exit(0) |
| 122 | + }, 1000) |
| 123 | + }, 1000) |
| 124 | +} |
| 125 | + |
| 126 | +// 3. Subscribe |
| 127 | +if (command === 'subscribe') { |
| 128 | + const pubKeyHex = argv._[1] |
| 129 | + if (!pubKeyHex) { |
| 130 | + console.error('Please provide public key hex') |
| 131 | + process.exit(1) |
| 132 | + } |
| 133 | + |
| 134 | + console.log(`Subscribing to ${pubKeyHex}...`) |
| 135 | + |
| 136 | + const client = new Client({ |
| 137 | + infoHash: Buffer.alloc(20), |
| 138 | + peerId: Buffer.alloc(20), |
| 139 | + announce: [argv.tracker], |
| 140 | + port: 6667 |
| 141 | + }) |
| 142 | + |
| 143 | + client.on('error', err => console.error('Client Error:', err.message)) |
| 144 | + |
| 145 | + // Hook into internal trackers to send subscribe |
| 146 | + setInterval(() => { |
| 147 | + const trackers = client._trackers |
| 148 | + for (const tracker of trackers) { |
| 149 | + if (tracker.socket && tracker.socket.readyState === 1 && !tracker._subscribed) { |
| 150 | + console.log('Sending subscribe to ' + tracker.announceUrl) |
| 151 | + tracker.socket.send(JSON.stringify({ |
| 152 | + action: 'subscribe', |
| 153 | + key: pubKeyHex |
| 154 | + })) |
| 155 | + tracker._subscribed = true // simple flag to avoid spamming |
| 156 | + |
| 157 | + // Listen for responses |
| 158 | + const originalOnMessage = tracker.socket.onmessage |
| 159 | + tracker.socket.onmessage = (event) => { |
| 160 | + let data |
| 161 | + try { data = JSON.parse(event.data) } catch (e) { return } |
| 162 | + |
| 163 | + if (data.action === 'publish') { |
| 164 | + console.log('\n>>> RECEIVED UPDATE <<<') |
| 165 | + const valid = validateManifest(data.manifest) |
| 166 | + if (valid && data.manifest.publicKey === pubKeyHex) { |
| 167 | + console.log('VERIFIED UPDATE from ' + pubKeyHex) |
| 168 | + console.log('Sequence:', data.manifest.sequence) |
| 169 | + console.log('Items:', data.manifest.collections[0].items) |
| 170 | + } else { |
| 171 | + console.error('Invalid signature or wrong key!') |
| 172 | + } |
| 173 | + } else { |
| 174 | + if (originalOnMessage) originalOnMessage(event) |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + }, 1000) |
| 180 | +} |
0 commit comments