UNPKG

@diva.exchange/i2p-sam

Version:

I2P SAM: peer-to-peer communication between applications over I2P

210 lines 8.24 kB
import { base32 } from 'rfc4648'; import crypto from 'crypto'; import { EventEmitter } from 'events'; import { Config } from './config.js'; import { Socket } from 'net'; const REPLY_HELLO = 'HELLOREPLY'; const REPLY_DEST = 'DESTREPLY'; const REPLY_SESSION = 'SESSIONSTATUS'; const REPLY_NAMING = 'NAMINGREPLY'; const REPLY_STREAM = 'STREAMSTATUS'; const KEY_RESULT = 'RESULT'; const KEY_PUB = 'PUB'; const KEY_PRIV = 'PRIV'; const KEY_DESTINATION = 'DESTINATION'; const KEY_VALUE = 'VALUE'; const VALUE_OK = 'OK'; export class I2pSam extends EventEmitter { constructor(c) { super(); this.socketControl = {}; this.timeout = 0; this.config = new Config(c); this.timeout = this.config.sam.timeout && this.config.sam.timeout >= 1 && this.config.sam.timeout <= 600 ? this.config.sam.timeout : 300; this.publicKey = this.config.sam.publicKey || ''; this.privateKey = this.config.sam.privateKey || ''; this.internalEventEmitter = new EventEmitter(); } async open() { this.socketControl = new Socket(); this.socketControl.on('data', (data) => { this.parseReply(data); }); this.socketControl.on('close', () => { this.emit('close'); }); try { await new Promise((resolve, reject) => { this.socketControl.once('error', reject); this.socketControl.connect({ host: this.config.sam.host, port: this.config.sam.portTCP }, () => { this.socketControl.removeAllListeners('error'); this.socketControl.on('error', (error) => { this.emit('error', error); }); resolve(this); }); }); await this.hello(this.socketControl); if (!this.publicKey || !this.privateKey) { await this.generateDestination(); } return this; } catch (e) { const error = e; return Promise.reject(new Error(error.toString())); } } close() { this.internalEventEmitter.removeAllListeners(); if (Object.keys(this.socketControl).length) { this.socketControl.destroy(); } } hello(socket) { return new Promise((resolve, reject) => { const min = this.config.sam.versionMin || false; const max = this.config.sam.versionMax || false; this.internalEventEmitter.removeAllListeners(); this.internalEventEmitter.once('error', reject); this.internalEventEmitter.once('hello', resolve); socket.write(`HELLO VERSION${min ? ' MIN=' + min : ''}${max ? ' MAX=' + max : ''}\n`, (error) => { if (error) { reject(error); } }); }); } initSession(type) { return new Promise((resolve, reject) => { let s = `SESSION CREATE ID=${this.config.session.id} DESTINATION=${this.privateKey} `; switch (type) { case 'STREAM': s += 'STYLE=STREAM'; break; case 'DATAGRAM': case 'RAW': s += `STYLE=${type} PORT=${this.config.listen.portForward} HOST=${this.config.listen.hostForward}`; break; } this.internalEventEmitter.removeAllListeners(); this.internalEventEmitter.once('error', reject); this.internalEventEmitter.once('session', resolve); s += (this.config.session.options ? ' ' + this.config.session.options : '') + '\n'; this.socketControl.write(s, (error) => { if (error) { reject(error); } }); }); } parseReply(data) { const sData = data.toString().trim(); const [c, s] = sData.split(' '); const oKeyValue = I2pSam.parseReplyKeyValue(sData); switch (c + s) { case REPLY_HELLO: return oKeyValue[KEY_RESULT] !== VALUE_OK ? this.internalEventEmitter.emit('error', new Error('HELLO failed: ' + sData)) : this.internalEventEmitter.emit('hello'); case REPLY_DEST: this.publicKey = oKeyValue[KEY_PUB] || ''; this.privateKey = oKeyValue[KEY_PRIV] || ''; return !this.publicKey || !this.privateKey ? this.internalEventEmitter.emit('error', new Error('DEST failed: ' + sData)) : this.internalEventEmitter.emit('destination'); case REPLY_SESSION: return oKeyValue[KEY_RESULT] !== VALUE_OK || !(oKeyValue[KEY_DESTINATION] || '') ? this.internalEventEmitter.emit('error', new Error('SESSION failed: ' + sData)) : this.internalEventEmitter.emit('session', this); case REPLY_NAMING: return oKeyValue[KEY_RESULT] !== VALUE_OK ? this.internalEventEmitter.emit('error', new Error('NAMING failed: ' + sData)) : this.internalEventEmitter.emit('naming', oKeyValue[KEY_VALUE]); case REPLY_STREAM: return oKeyValue[KEY_RESULT] !== VALUE_OK ? this.internalEventEmitter.emit('error', new Error('STREAM failed: ' + sData)) : this.internalEventEmitter.emit('stream'); default: return; } } static parseReplyKeyValue(data) { const [...args] = data.split(' '); const objResult = {}; for (const s of args.filter((s) => s.indexOf('=') > -1)) { const [k, v] = s.split('='); objResult[k.trim()] = v.trim(); } return objResult; } generateDestination() { this.publicKey = ''; this.privateKey = ''; return new Promise((resolve, reject) => { this.internalEventEmitter.removeAllListeners(); this.internalEventEmitter.once('error', (error) => { reject(error); }); this.internalEventEmitter.once('destination', resolve); this.socketControl.write('DEST GENERATE\n', (error) => { if (error) { this.internalEventEmitter.emit('error', error); } }); }); } resolve(name) { return new Promise((resolve, reject) => { if (!/\.i2p$/.test(name)) { reject(new Error('Invalid I2P address: ' + name)); } this.internalEventEmitter.removeAllListeners(); this.internalEventEmitter.once('error', (error) => { reject(error); }); this.internalEventEmitter.once('naming', resolve); this.socketControl.write(`NAMING LOOKUP NAME=${name}\n`, (error) => { if (error) { this.internalEventEmitter.emit('error', error); } }); }); } getB32Address() { return I2pSam.toB32(this.publicKey) + '.b32.i2p'; } getPublicKey() { return this.publicKey; } getPrivateKey() { return this.privateKey; } getKeyPair() { return { public: this.getPublicKey(), private: this.getPrivateKey(), }; } static toB32(base64Destination) { const s = Buffer.from(base64Destination.replace(/-/g, '+').replace(/~/g, '/'), 'base64'); return base32.stringify(crypto.createHash('sha256').update(s).digest(), { pad: false }).toLowerCase(); } static async createLocalDestination(c) { const sam = new I2pSam(c); await sam.open(); sam.close(); return { address: sam.getB32Address(), public: sam.getPublicKey(), private: sam.getPrivateKey() }; } static async lookup(c, address) { const sam = new I2pSam(c); await sam.open(); const s = await sam.resolve(address); sam.close(); return s; } } //# sourceMappingURL=i2p-sam.js.map