File Explorer

/proc/2/root/var/runtime/node_modules/@aws-sdk/node_modules/undici/lib/core

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

socks5-client.js11.6 KB · 423 lines
'use strict' const { EventEmitter } = require('node:events')const { Buffer } = require('node:buffer')const { InvalidArgumentError, Socks5ProxyError } = require('./errors')const { debuglog } = require('node:util')const { parseAddress } = require('./socks5-utils') const debug = debuglog('undici:socks5')const EMPTY_BUFFER = Buffer.alloc(0) // SOCKS5 constantsconst SOCKS_VERSION = 0x05 // Authentication methodsconst AUTH_METHODS = {  NO_AUTH: 0x00,  GSSAPI: 0x01,  USERNAME_PASSWORD: 0x02,  NO_ACCEPTABLE: 0xFF} // SOCKS5 commandsconst COMMANDS = {  CONNECT: 0x01,  BIND: 0x02,  UDP_ASSOCIATE: 0x03} // Address typesconst ADDRESS_TYPES = {  IPV4: 0x01,  DOMAIN: 0x03,  IPV6: 0x04} // Reply codesconst REPLY_CODES = {  SUCCEEDED: 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} // State machine statesconst STATES = {  INITIAL: 'initial',  HANDSHAKING: 'handshaking',  AUTHENTICATING: 'authenticating',  AUTHENTICATED: 'authenticated',  CONNECTING: 'connecting',  CONNECTED: 'connected',  ERROR: 'error',  CLOSED: 'closed'} /** * SOCKS5 client implementation * Handles SOCKS5 protocol negotiation and connection establishment */class Socks5Client extends EventEmitter {  constructor (socket, options = {}) {    super()     if (!socket) {      throw new InvalidArgumentError('socket is required')    }     this.socket = socket    this.options = options    this.state = STATES.INITIAL    this.buffer = EMPTY_BUFFER    this.onSocketData = this.onData.bind(this)    this.onSocketError = this.onError.bind(this)    this.onSocketClose = this.onClose.bind(this)     // Authentication settings    this.authMethods = []    if (options.username && options.password) {      this.authMethods.push(AUTH_METHODS.USERNAME_PASSWORD)    }    this.authMethods.push(AUTH_METHODS.NO_AUTH)     // Socket event handlers    this.socket.on('data', this.onSocketData)    this.socket.on('error', this.onSocketError)    this.socket.on('close', this.onSocketClose)  }   /**   * Handle incoming data from the socket   */  onData (data) {    debug('received data', data.length, 'bytes in state', this.state)    this.buffer = Buffer.concat([this.buffer, data])     try {      switch (this.state) {        case STATES.HANDSHAKING:          this.handleHandshakeResponse()          break        case STATES.AUTHENTICATING:          this.handleAuthResponse()          break        case STATES.CONNECTING:          this.handleConnectResponse()          break      }    } catch (err) {      this.onError(err)    }  }   /**   * Handle socket errors   */  onError (err) {    debug('socket error', err)    this.state = STATES.ERROR    this.emit('error', err)    this.destroy()  }   /**   * Handle socket close   */  onClose () {    debug('socket closed')    this.state = STATES.CLOSED    this.emit('close')  }   /**   * Destroy the client and underlying socket   */  destroy () {    if (this.socket && !this.socket.destroyed) {      this.socket.destroy()    }  }   markAuthenticated () {    this.state = STATES.AUTHENTICATED    this.emit('authenticated')  }   /**   * Start the SOCKS5 handshake   */  handshake () {    if (this.state !== STATES.INITIAL) {      throw new InvalidArgumentError('Handshake already started')    }     debug('starting handshake with', this.authMethods.length, 'auth methods')    this.state = STATES.HANDSHAKING     // Build handshake request    // +----+----------+----------+    // |VER | NMETHODS | METHODS  |    // +----+----------+----------+    // | 1  |    1     | 1 to 255 |    // +----+----------+----------+    const request = Buffer.alloc(2 + this.authMethods.length)    request[0] = SOCKS_VERSION    request[1] = this.authMethods.length    this.authMethods.forEach((method, i) => {      request[2 + i] = method    })     this.socket.write(request)  }   /**   * Handle handshake response from server   */  handleHandshakeResponse () {    if (this.buffer.length < 2) {      return // Not enough data yet    }     const version = this.buffer[0]    const method = this.buffer[1]     if (version !== SOCKS_VERSION) {      throw new Socks5ProxyError(`Invalid SOCKS version: ${version}`, 'UND_ERR_SOCKS5_VERSION')    }     if (method === AUTH_METHODS.NO_ACCEPTABLE) {      throw new Socks5ProxyError('No acceptable authentication method', 'UND_ERR_SOCKS5_AUTH_REJECTED')    }     this.buffer = this.buffer.subarray(2)    debug('server selected auth method', method)     if (method === AUTH_METHODS.NO_AUTH) {      this.markAuthenticated()    } else if (method === AUTH_METHODS.USERNAME_PASSWORD) {      this.state = STATES.AUTHENTICATING      this.sendAuthRequest()    } else {      throw new Socks5ProxyError(`Unsupported authentication method: ${method}`, 'UND_ERR_SOCKS5_AUTH_METHOD')    }  }   /**   * Send username/password authentication request   */  sendAuthRequest () {    const { username, password } = this.options     if (!username || !password) {      throw new InvalidArgumentError('Username and password required for authentication')    }     debug('sending username/password auth')     // Username/Password authentication request (RFC 1929)    // +----+------+----------+------+----------+    // |VER | ULEN |  UNAME   | PLEN |  PASSWD  |    // +----+------+----------+------+----------+    // | 1  |  1   | 1 to 255 |  1   | 1 to 255 |    // +----+------+----------+------+----------+    const usernameBuffer = Buffer.from(username)    const passwordBuffer = Buffer.from(password)     if (usernameBuffer.length > 255 || passwordBuffer.length > 255) {      throw new InvalidArgumentError('Username or password too long')    }     const request = Buffer.alloc(3 + usernameBuffer.length + passwordBuffer.length)    request[0] = 0x01 // Sub-negotiation version    request[1] = usernameBuffer.length    usernameBuffer.copy(request, 2)    request[2 + usernameBuffer.length] = passwordBuffer.length    passwordBuffer.copy(request, 3 + usernameBuffer.length)     this.socket.write(request)  }   /**   * Handle authentication response   */  handleAuthResponse () {    if (this.buffer.length < 2) {      return // Not enough data yet    }     const version = this.buffer[0]    const status = this.buffer[1]     if (version !== 0x01) {      throw new Socks5ProxyError(`Invalid auth sub-negotiation version: ${version}`, 'UND_ERR_SOCKS5_AUTH_VERSION')    }     if (status !== 0x00) {      throw new Socks5ProxyError('Authentication failed', 'UND_ERR_SOCKS5_AUTH_FAILED')    }     this.buffer = this.buffer.subarray(2)    debug('authentication successful')    this.markAuthenticated()  }   /**   * Send CONNECT command   * @param {string} address - Target address (IP or domain)   * @param {number} port - Target port   */  connect (address, port) {    if (this.state === STATES.CONNECTING || this.state === STATES.CONNECTED) {      throw new InvalidArgumentError('Connection already in progress')    }     if (this.state !== STATES.AUTHENTICATED) {      throw new InvalidArgumentError('Client must be authenticated before CONNECT')    }     debug('connecting to', address, port)    this.state = STATES.CONNECTING     const request = this.buildConnectRequest(COMMANDS.CONNECT, address, port)    this.socket.write(request)  }   /**   * Build a SOCKS5 request   */  buildConnectRequest (command, address, port) {    // Parse address to determine type and buffer    const { type: addressType, buffer: addressBuffer } = parseAddress(address)     // Build request    // +----+-----+-------+------+----------+----------+    // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |    // +----+-----+-------+------+----------+----------+    // | 1  |  1  | X'00' |  1   | Variable |    2     |    // +----+-----+-------+------+----------+----------+    const request = Buffer.alloc(4 + addressBuffer.length + 2)    request[0] = SOCKS_VERSION    request[1] = command    request[2] = 0x00 // Reserved    request[3] = addressType    addressBuffer.copy(request, 4)    request.writeUInt16BE(port, 4 + addressBuffer.length)     return request  }   /**   * Handle CONNECT response   */  handleConnectResponse () {    if (this.buffer.length < 4) {      return // Not enough data for header    }     const version = this.buffer[0]    const reply = this.buffer[1]    const addressType = this.buffer[3]     if (version !== SOCKS_VERSION) {      throw new Socks5ProxyError(`Invalid SOCKS version in reply: ${version}`, 'UND_ERR_SOCKS5_REPLY_VERSION')    }     // Calculate the expected response length    let responseLength = 4 // VER + REP + RSV + ATYP    if (addressType === ADDRESS_TYPES.IPV4) {      responseLength += 4 + 2 // IPv4 + port    } else if (addressType === ADDRESS_TYPES.DOMAIN) {      if (this.buffer.length < 5) {        return // Need domain length byte      }      responseLength += 1 + this.buffer[4] + 2 // length byte + domain + port    } else if (addressType === ADDRESS_TYPES.IPV6) {      responseLength += 16 + 2 // IPv6 + port    } else {      throw new Socks5ProxyError(`Invalid address type in reply: ${addressType}`, 'UND_ERR_SOCKS5_ADDR_TYPE')    }     if (this.buffer.length < responseLength) {      return // Not enough data for full response    }     if (reply !== REPLY_CODES.SUCCEEDED) {      const errorMessage = this.getReplyErrorMessage(reply)      throw new Socks5ProxyError(`SOCKS5 connection failed: ${errorMessage}`, `UND_ERR_SOCKS5_REPLY_${reply}`)    }     // Parse bound address and port    let boundAddress    let offset = 4     if (addressType === ADDRESS_TYPES.IPV4) {      boundAddress = Array.from(this.buffer.subarray(offset, offset + 4)).join('.')      offset += 4    } else if (addressType === ADDRESS_TYPES.DOMAIN) {      const domainLength = this.buffer[offset]      offset += 1      boundAddress = this.buffer.subarray(offset, offset + domainLength).toString()      offset += domainLength    } else if (addressType === ADDRESS_TYPES.IPV6) {      // Parse IPv6 address from 16-byte buffer      const parts = []      for (let i = 0; i < 8; i++) {        const value = this.buffer.readUInt16BE(offset + i * 2)        parts.push(value.toString(16))      }      boundAddress = parts.join(':')      offset += 16    }     const boundPort = this.buffer.readUInt16BE(offset)     this.buffer = EMPTY_BUFFER    this.state = STATES.CONNECTED    this.socket.removeListener('data', this.onSocketData)     debug('connected, bound address:', boundAddress, 'port:', boundPort)    this.emit('connected', { address: boundAddress, port: boundPort })  }   /**   * Get human-readable error message for reply code   */  getReplyErrorMessage (reply) {    switch (reply) {      case REPLY_CODES.GENERAL_FAILURE:        return 'General SOCKS server failure'      case REPLY_CODES.CONNECTION_NOT_ALLOWED:        return 'Connection not allowed by ruleset'      case REPLY_CODES.NETWORK_UNREACHABLE:        return 'Network unreachable'      case REPLY_CODES.HOST_UNREACHABLE:        return 'Host unreachable'      case REPLY_CODES.CONNECTION_REFUSED:        return 'Connection refused'      case REPLY_CODES.TTL_EXPIRED:        return 'TTL expired'      case REPLY_CODES.COMMAND_NOT_SUPPORTED:        return 'Command not supported'      case REPLY_CODES.ADDRESS_TYPE_NOT_SUPPORTED:        return 'Address type not supported'      default:        return `Unknown error code: ${reply}`    }  }} module.exports = {  Socks5Client,  AUTH_METHODS,  COMMANDS,  ADDRESS_TYPES,  REPLY_CODES,  STATES}