OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident / socks-server.js

508 lines · javascript ref:
#!/usr/bin/env node
/** @typedef {import('pear-interface')} */ /* global Pear */

/**
 * Hyperswarm SOCKS Proxy with Hyperident Profile Support
 * Provides SOCKS5 proxy functionality with deterministic addressing via profiles
 */

import { createServer } from 'net'
import { randomBytes } from 'crypto'
import { IdentityManager } from './lib/identity-manager.js'

/**
 * SOCKS5 constants
 */
const SOCKS_VERSION = 0x05
const SOCKS_METHODS = {
  NO_AUTH: 0x00,
  NO_METHODS: 0xFF
}
const SOCKS_COMMANDS = {
  CONNECT: 0x01
}
const SOCKS_REPLIES = {
  SUCCESS: 0x00,
  GENERAL_FAILURE: 0x01,
  CONNECTION_NOT_ALLOWED: 0x02,
  NETWORK_UNREACHABLE: 0x03,
  HOST_UNREACHABLE: 0x04,
  CONNECTION_REFUSED: 0x05,
  TTL_EXPIRED: 0x06,
  COMMAND_NOT_SUPPORTED: 0x07,
  ADDRESS_TYPE_NOT_SUPPORTED: 0x08
}
const SOCKS_ADDRESS_TYPES = {
  IPV4: 0x01,
  DOMAIN: 0x03,
  IPV6: 0x04
}

/**
 * Simple hyperswarm mock for demonstration
 * In a real implementation, this would use actual hyperswarm
 */
class HyperswarmMock {
  constructor(topic) {
    this.topic = topic
    this.connections = new Set()
  }

  createServer() {
    const server = createServer()
    server.on('connection', (socket) => {
      this.connections.add(socket)
      socket.on('close', () => {
        this.connections.delete(socket)
      })
    })
    return server
  }

  connect(topic) {
    // Mock connection - in real implementation would connect to hyperswarm
    return new Promise((resolve) => {
      const mockSocket = createServer().listen(0)
      setTimeout(() => resolve(mockSocket), 100)
    })
  }

  destroy() {
    this.connections.forEach(conn => conn.destroy())
    this.connections.clear()
  }
}

/**
 * SOCKS5 Proxy Server
 */
class SocksProxy {
  constructor(options = {}) {
    this.port = options.port || 1080
    this.host = options.host || '127.0.0.1'
    this.topic = options.topic || null
    this.profileName = options.profileName || null
    this.server = null
    this.hyperswarm = null
  }

  /**
   * Start the SOCKS proxy server
   */
  async start() {
    this.server = createServer()
    this.server.on('connection', this.handleConnection.bind(this))
    
    // Initialize hyperswarm with topic
    if (this.topic) {
      this.hyperswarm = new HyperswarmMock(this.topic)
    }

    return new Promise((resolve, reject) => {
      this.server.listen(this.port, this.host, (err) => {
        if (err) {
          reject(err)
        } else {
          resolve()
        }
      })
    })
  }

  /**
   * Stop the SOCKS proxy server
   */
  async stop() {
    if (this.hyperswarm) {
      this.hyperswarm.destroy()
    }
    
    if (this.server) {
      return new Promise((resolve) => {
        this.server.close(resolve)
      })
    }
  }

  /**
   * Handle incoming SOCKS connection
   */
  handleConnection(socket) {
    socket.on('data', (data) => {
      this.handleSocksData(socket, data)
    })

    socket.on('error', (err) => {
      console.error('Socket error:', err.message)
    })

    socket.on('close', () => {
      // Connection closed
    })
  }

  /**
   * Handle SOCKS protocol data
   */
  handleSocksData(socket, data) {
    if (data.length < 2) {
      socket.destroy()
      return
    }

    const version = data[0]
    if (version !== SOCKS_VERSION) {
      socket.destroy()
      return
    }

    // Check if this is authentication negotiation
    if (data.length >= 2 && data[1] > 0) {
      this.handleAuth(socket, data)
    } else {
      this.handleConnect(socket, data)
    }
  }

  /**
   * Handle SOCKS authentication
   */
  handleAuth(socket, data) {
    const numMethods = data[1]
    if (data.length < 2 + numMethods) {
      socket.destroy()
      return
    }

    const methods = Array.from(data.slice(2, 2 + numMethods))
    
    // We only support NO_AUTH
    if (methods.includes(SOCKS_METHODS.NO_AUTH)) {
      const response = Buffer.from([SOCKS_VERSION, SOCKS_METHODS.NO_AUTH])
      socket.write(response)
    } else {
      const response = Buffer.from([SOCKS_VERSION, SOCKS_METHODS.NO_METHODS])
      socket.write(response)
      socket.destroy()
    }
  }

  /**
   * Handle SOCKS CONNECT command
   */
  handleConnect(socket, data) {
    if (data.length < 4) {
      socket.destroy()
      return
    }

    const version = data[0]
    const command = data[1]
    const addressType = data[3]

    if (version !== SOCKS_VERSION || command !== SOCKS_COMMANDS.CONNECT) {
      this.sendConnectResponse(socket, SOCKS_REPLIES.COMMAND_NOT_SUPPORTED)
      return
    }

    let address, port, offset
    
    try {
      switch (addressType) {
        case SOCKS_ADDRESS_TYPES.IPV4:
          if (data.length < 10) throw new Error('Invalid IPv4 address')
          address = Array.from(data.slice(4, 8)).join('.')
          port = data.readUInt16BE(8)
          offset = 10
          break
          
        case SOCKS_ADDRESS_TYPES.DOMAIN:
          if (data.length < 5) throw new Error('Invalid domain address')
          const domainLength = data[4]
          if (data.length < 5 + domainLength + 2) throw new Error('Invalid domain address')
          address = data.slice(5, 5 + domainLength).toString()
          port = data.readUInt16BE(5 + domainLength)
          offset = 7 + domainLength
          break
          
        case SOCKS_ADDRESS_TYPES.IPV6:
          if (data.length < 22) throw new Error('Invalid IPv6 address')
          // IPv6 handling would go here
          this.sendConnectResponse(socket, SOCKS_REPLIES.ADDRESS_TYPE_NOT_SUPPORTED)
          return
          
        default:
          this.sendConnectResponse(socket, SOCKS_REPLIES.ADDRESS_TYPE_NOT_SUPPORTED)
          return
      }

      // For demo purposes, we'll just send success response
      // In real implementation, this would establish hyperswarm connection
      this.sendConnectResponse(socket, SOCKS_REPLIES.SUCCESS, address, port)
      
    } catch (error) {
      console.error('Connect error:', error.message)
      this.sendConnectResponse(socket, SOCKS_REPLIES.GENERAL_FAILURE)
    }
  }

  /**
   * Send SOCKS CONNECT response
   */
  sendConnectResponse(socket, reply, address = '0.0.0.0', port = 0) {
    const response = Buffer.alloc(10)
    response[0] = SOCKS_VERSION
    response[1] = reply
    response[2] = 0x00 // Reserved
    response[3] = SOCKS_ADDRESS_TYPES.IPV4
    
    // Write IP address (4 bytes)
    const ipParts = address.split('.').map(part => parseInt(part) || 0)
    response[4] = ipParts[0]
    response[5] = ipParts[1]
    response[6] = ipParts[2]
    response[7] = ipParts[3]
    
    // Write port (2 bytes)
    response.writeUInt16BE(port, 8)
    
    socket.write(response)
    
    if (reply !== SOCKS_REPLIES.SUCCESS) {
      socket.destroy()
    }
  }
}

/**
 * Parse command line arguments
 */
function parseArgs(argv) {
  const args = argv.slice(2)
  const parsed = {
    command: null,
    flags: {},
    positional: []
  }

  for (let i = 0; i < args.length; i++) {
    const arg = args[i]
    
    if (arg === 'server') {
      parsed.command = 'server'
    } else if (arg === '--profile') {
      if (i + 1 < args.length) {
        parsed.flags.profile = args[i + 1]
        i++ // Skip next argument
      } else {
        throw new Error('--profile flag requires a profile name')
      }
    } else if (arg === '--port') {
      if (i + 1 < args.length) {
        parsed.flags.port = parseInt(args[i + 1])
        i++ // Skip next argument
      } else {
        throw new Error('--port flag requires a port number')
      }
    } else if (arg === '--help' || arg === '-h') {
      parsed.command = 'help'
    } else if (!arg.startsWith('-')) {
      parsed.positional.push(arg)
    }
  }

  return parsed
}

/**
 * Show help message
 */
function showHelp() {
  console.log(`
🌐 Hyperswarm SOCKS Proxy with Hyperident Integration

USAGE:
  node socks-server.js server [options]

OPTIONS:
  --profile <name>    Use deterministic address from Hyperident profile
  --port <number>     SOCKS proxy port (default: 1080)
  --help, -h          Show this help

EXAMPLES:
  node socks-server.js server
  # ✅ Hyperswarm SOCKS proxy started
  # 🔡 Address: [random-hex].hyperswarm
  # 🌐 SOCKS proxy: 127.0.0.1:1080

  node socks-server.js server --profile work
  # ✅ Hyperswarm SOCKS proxy started
  # 🔡 Address: [deterministic-from-work].hyperswarm  
  # 🌐 SOCKS proxy: 127.0.0.1:1080

HYPERIDENT INTEGRATION:
  The --profile flag uses Hyperident for deterministic addressing:
  1. Loads identity from ~/.hyperident/
  2. Derives topic from profile name
  3. Uses consistent hyperswarm address

  Setup Hyperident first:
    hyperident init
    hyperident create-profile work
`)
}

/**
 * Get hidden input for passphrase
 */
function getHiddenInput(prompt) {
  return new Promise((resolve) => {
    process.stdout.write(prompt)
    process.stdin.setRawMode(true)
    process.stdin.resume()
    process.stdin.setEncoding('utf8')
    
    let input = ''
    const onData = (char) => {
      if (char === '\u0003') { // Ctrl+C
        process.exit(1)
      } else if (char === '\r' || char === '\n') {
        process.stdin.setRawMode(false)
        process.stdin.pause()
        process.stdin.removeListener('data', onData)
        process.stdout.write('\n')
        resolve(input)
      } else if (char === '\u007f') { // Backspace
        if (input.length > 0) {
          input = input.slice(0, -1)
          process.stdout.write('\b \b')
        }
      } else {
        input += char
        process.stdout.write('*')
      }
    }
    
    process.stdin.on('data', onData)
  })
}

/**
 * Main server function
 */
async function main() {
  try {
    const parsed = parseArgs(process.argv)
    
    if (parsed.command === 'help' || !parsed.command) {
      showHelp()
      return
    }
    
    if (parsed.command !== 'server') {
      console.error('❌ Error: Unknown command. Use "server" or --help')
      process.exit(1)
    }

    const port = parsed.flags.port || 1080
    const profileName = parsed.flags.profile
    let topic = null
    let address = null

    // If profile specified, get deterministic topic from Hyperident
    if (profileName) {
      console.log(`🔍 Loading profile '${profileName}'...`)
      
      try {
        // Check if identity is encrypted and prompt for passphrase if needed
        let passphrase = null
        try {
          await IdentityManager.loadIdentity()
        } catch (error) {
          if (error.message.includes('encrypted but no passphrase provided')) {
            console.log('🔐 Identity is encrypted. Please enter your passphrase.')
            passphrase = await getHiddenInput('Enter passphrase: ')
          } else {
            throw error
          }
        }
        
        // Get the hyperswarm address for the profile
        address = await IdentityManager.getProfileAddress(profileName, passphrase)
        topic = Buffer.from(address, 'hex')
        
        console.log(`✅ Profile '${profileName}' loaded`)
        console.log(`🔡 Deterministic address: ${address}`)
        
      } catch (error) {
        console.error('❌ Failed to load profile:', error.message)
        
        if (error.message.includes('Identity not found')) {
          console.error('💡 Run "hyperident init" first to create an identity')
        } else if (error.message.includes('not found')) {
          console.error('💡 Create the profile: hyperident create-profile ' + profileName)
          console.error('   Or list available profiles: hyperident list-profiles')
        } else if (error.message.includes('Invalid passphrase')) {
          console.error('💡 Please check your passphrase and try again')
        }
        
        process.exit(1)
      }
    } else {
      // Generate random topic for non-profile usage
      topic = randomBytes(32)
      address = topic.toString('hex')
      console.log(`🎲 Using random address: ${address}`)
    }

    // Create and start SOCKS proxy
    const proxy = new SocksProxy({
      port,
      topic,
      profileName
    })

    console.log('🚀 Starting Hyperswarm SOCKS proxy...')
    await proxy.start()
    
    console.log('✅ Hyperswarm SOCKS proxy started')
    console.log(`🔡 Address: ${address}`)
    console.log(`🌐 SOCKS proxy: 127.0.0.1:${port}`)
    console.log('')
    console.log('💡 Configure your applications to use SOCKS5 proxy:')
    console.log(`   Host: 127.0.0.1`)
    console.log(`   Port: ${port}`)
    console.log('')
    console.log('Press Ctrl+C to stop')

    // Handle graceful shutdown
    process.on('SIGINT', async () => {
      console.log('\n🛑 Shutting down...')
      await proxy.stop()
      console.log('✅ SOCKS proxy stopped')
      process.exit(0)
    })

    process.on('SIGTERM', async () => {
      console.log('\n🛑 Shutting down...')
      await proxy.stop()
      console.log('✅ SOCKS proxy stopped')
      process.exit(0)
    })

  } catch (error) {
    console.error('❌ Error:', error.message)
    if (process.env.DEBUG) {
      console.error(error.stack)
    }
    process.exit(1)
  }
}

// Run if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
  main()
}

export { SocksProxy, parseArgs, showHelp, main }