lib_network.js

/**
 * @module bulb/network
 */
'use strict';

const { Server } = require('node:net');
const { Agent } = require('node:http');
const { Duplex } = require('node:stream');
const { SocksClient } = require('socks');


class HiddenSocket extends Duplex {

  /**
   * Wraps a net.Socket in a stream.Duplex that passes data through the supplied
   * context's tor SOCKSv5 proxy.
   * @param {module:bulb/context~TorContext} context
   */
  constructor(context) {
    super();

    this._context = context;
  }

  /**
   * @typedef {object} module:bulb/network~HiddenSocket~ConnectOptions
   * @property {string} host - Address (including onion URLs) to connect to.
   * @property {number} port - Port to connect to.
   */

  /**
   * Connects the underlying socket to the destination through a 
   * tor SOCKSv5 proxy.
   * @param {module:bulb/network~HiddenSocket~ConnectOptions} options - Destination connection address
   */ 
  connect(options) {
    return new Promise(async (resolve) => {
      this._socksPort = await this._context.readSocksPort();
      this._socksClient = await SocksClient.createConnection({
        proxy: {
          host: '127.0.0.1',
          type: 5,
          port: this._socksPort
        },
        command: 'connect',
        destination: {
          host: options.host,
          port: options.port
        }
      });
      this._socksProxy = this._socksClient.socket;

      this._socksProxy.on('data', d => this.push(d));
      this._socksProxy.on('error', e => this.emit('error', e));
      this._socksProxy.on('close', () => this.push(null));

      resolve(this);
    });
  }

  _write(data, enc, done) {
    this._socksProxy.write(data, enc, done);
  }

  _read() {}

}


class HiddenServer {

  /**
   * Handles incoming connections.
   * @callback module:bulb/network~HiddenServer~ConnectionListener
   * @param {net.Socket} connection
   */

  /**
   * Wraps a net.Server, exposing it as an onion service. 
   * @param {module:bulb/context~TorContext} context - Tor context to create onion.
   * @param {module:bulb/network~HiddenServer~ConnectionListener} [connectionLister] - Function to handle incoming connections 
   */
  constructor(context, ...serverArgs) {
    this._context = context;
    this._server = new Server(...serverArgs);
    this._address = {
      host: null,
      port: 0
    };
  }

  /**
   * Calls the underlying net.Socket#listen, then establishes a connection
   * as on onion service using the current tor context.
   * @param {module:bulb/commands~ControlCommand~AddOnionOptions}
   * @returns {Promise<module:bulb/network~HiddenServer~Address>}
   */
  listen(addOnionOptions = {}) { 
    return new Promise((resolve, reject) => {
      this._server.listen(0, () => {

        if (!addOnionOptions.virtualPort) {
          addOnionOptions.virtualPort = this._server.address().port;
        }

        this._context.control.createHiddenService(
          '127.0.0.1:' + this._server.address().port,
          addOnionOptions
        ).then(result => {
          this._address.host = result.serviceId + '.onion';
          this._address.port = addOnionOptions.virtualPort;
          this.privateKey = result.privateKey;
          this.serviceId = result.serviceId;

          resolve(this.address());
        }, reject); 
      })
    });
  }

  /**
   * @typedef {object} module:bulb/network~HiddenServer~Address
   * @property {string} host - Onion address for this service.
   * @property {number} port - Virtual port on the onion service.
   */

  /**
   * Returns the hidden service's address
   * @returns {module:bulb/network~HiddenServer~Address}
   */
  address() {
    return this._address;
  }

  get server() {
    return this._server;
  }

}


class TorHttpAgent extends Agent {

  /**
   * HTTP request agent for proxing requests through Tor.
   * @param {module:bulb/context~TorContext} context - Tor context to proxy through.
   */
  constructor(context) {
    super(options);
    this._context = context;
  }

  /**
   * Implements http.Agent#createConnection to use a {@link module:bulb/network~HiddenSocket}. 
   * You probably won't use this directly, instead passing this instance to http#request.
   * @param {module:bulb/network~HiddenSocket~ConnectOptions} options
   * @param {function} [connectListener]
   */
  createConnection(options, callback) {
    const hs = new HiddenSocket(this._context);
    hs.connect(options).then(() => callback(null, hs), callback);
  }

}

module.exports.HiddenSocket = HiddenSocket;
module.exports.HiddenServer = HiddenServer;
module.exports.TorHttpAgent = TorHttpAgent;