module Excon::SOCKS5
SOCKS5 protocol implementation (RFC 1928, RFC 1929) Shared module for SOCKS5Socket and SOCKS5SSLSocket
Constants
- MAX_HOSTNAME_LENGTH
Maximum hostname length per RFC 1928
- SOCKS5_ATYP_DOMAIN
- SOCKS5_ATYP_IPV4
Address types
- SOCKS5_ATYP_IPV6
- SOCKS5_AUTH_USERNAME_PASSWORD
- SOCKS5_CMD_CONNECT
Commands
- SOCKS5_ERRORS
- SOCKS5_NO_ACCEPTABLE_AUTH
- SOCKS5_NO_AUTH
Authentication methods
- SOCKS5_RESERVED
- SOCKS5_SUCCESS
Reply codes
- SOCKS5_VERSION
Private Instance Methods
parse_socks5_proxy(proxy_string)
click to toggle source
Parse SOCKS5 proxy string into components @param proxy_string [String] Proxy specification in various formats @return [Array<String, String, String, String>] host, port, user, pass
# File lib/excon/socks5.rb, line 44 def parse_socks5_proxy(proxy_string) # Support formats: # host:port # user:pass@host:port # socks5://host:port # socks5://user:pass@host:port proxy_string = proxy_string.to_s.sub(%r{^socks5://}, '') user = nil pass = nil if proxy_string.include?('@') auth, host_port = proxy_string.split('@', 2) user, pass = auth.split(':', 2) else host_port = proxy_string end host, port = host_port.split(':', 2) port ||= '1080' [host, port, user, pass] end
socks5_authenticate()
click to toggle source
Perform SOCKS5 authentication handshake
# File lib/excon/socks5.rb, line 69 def socks5_authenticate auth_methods = if @proxy_user && @proxy_pass [SOCKS5_NO_AUTH, SOCKS5_AUTH_USERNAME_PASSWORD] else [SOCKS5_NO_AUTH] end greeting = [SOCKS5_VERSION, auth_methods.length, *auth_methods].pack('C*') @socket.write(greeting) response = socks5_read_exactly(2) version, chosen_method = response.unpack('CC') if version != SOCKS5_VERSION raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy returned invalid version: #{version}")) end case chosen_method when SOCKS5_NO_AUTH # No authentication required when SOCKS5_AUTH_USERNAME_PASSWORD unless @proxy_user && @proxy_pass raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy requires authentication but no credentials provided')) end socks5_username_password_auth when SOCKS5_NO_ACCEPTABLE_AUTH raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy: no acceptable authentication methods')) else raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy: unsupported authentication method #{chosen_method}")) end end
socks5_connect(host, port)
click to toggle source
Request connection to target through SOCKS5 proxy
# File lib/excon/socks5.rb, line 122 def socks5_connect(host, port) if host.bytesize > MAX_HOSTNAME_LENGTH raise Excon::Error::Socket.new(Exception.new("SOCKS5: hostname exceeds maximum length of #{MAX_HOSTNAME_LENGTH} bytes")) end # Build CONNECT request with domain name (let proxy resolve DNS) request = [SOCKS5_VERSION, SOCKS5_CMD_CONNECT, SOCKS5_RESERVED].pack('CCC') request += [SOCKS5_ATYP_DOMAIN, host.bytesize, host].pack('CCA*') request += [port.to_i].pack('n') @socket.write(request) response = socks5_read_exactly(4) version, reply, _, atyp = response.unpack('CCCC') if version != SOCKS5_VERSION raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy returned invalid version: #{version}")) end unless reply == SOCKS5_SUCCESS error_msg = SOCKS5_ERRORS[reply] || "Unknown error (#{reply})" raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy connect failed: #{error_msg}")) end # Read and discard bound address (not needed for CONNECT) socks5_read_bound_address(atyp) end
socks5_read_bound_address(atyp)
click to toggle source
# File lib/excon/socks5.rb, line 150 def socks5_read_bound_address(atyp) case atyp when SOCKS5_ATYP_IPV4 socks5_read_exactly(4 + 2) # 4 bytes IP + 2 bytes port when SOCKS5_ATYP_DOMAIN domain_len = socks5_read_exactly(1).unpack1('C') socks5_read_exactly(domain_len + 2) when SOCKS5_ATYP_IPV6 socks5_read_exactly(16 + 2) # 16 bytes IP + 2 bytes port else raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy returned unknown address type: #{atyp}")) end end
socks5_read_exactly(nbytes)
click to toggle source
Read exact number of bytes with timeout support
# File lib/excon/socks5.rb, line 165 def socks5_read_exactly(nbytes) data = ''.dup deadline = @data[:read_timeout] ? Time.now + @data[:read_timeout] : nil while data.bytesize < nbytes if deadline remaining = deadline - Time.now if remaining <= 0 raise Excon::Error::Timeout.new('SOCKS5 read timeout') end ready = IO.select([@socket], nil, nil, remaining) unless ready raise Excon::Error::Timeout.new('SOCKS5 read timeout') end end chunk = @socket.read_nonblock(nbytes - data.bytesize, exception: false) case chunk when :wait_readable IO.select([@socket], nil, nil, deadline ? [deadline - Time.now, 0].max : nil) when nil, '' raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy connection closed unexpectedly')) else data << chunk end end data end
socks5_username_password_auth()
click to toggle source
RFC 1929: Username/Password Authentication
# File lib/excon/socks5.rb, line 102 def socks5_username_password_auth auth_request = [ 0x01, # auth protocol version @proxy_user.bytesize, @proxy_user, @proxy_pass.bytesize, @proxy_pass ].pack('CCA*CA*') @socket.write(auth_request) response = socks5_read_exactly(2) _, status = response.unpack('CC') unless status == 0x00 raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy authentication failed')) end end