http icon indicating copy to clipboard operation
http copied to clipboard

Socks 5 support

Open sebyx07 opened this issue 7 months ago • 7 comments

socks 5 support,

example:

# SOCKS5 proxy details
proxy_host = "98.170.57.241"
proxy_port = 4145

begin
  # Make the request through SOCKS5 proxy
  response = HTTP
             .via_socks5(proxy_host, proxy_port)
             .get("http://ifconfig.me/ip")

  puts "Response status: #{response.status}"
  puts "Response body: #{response.body}"
rescue => e
  puts "Error: #{e.class} - #{e.message}"
  puts e.backtrace
end

sebyx07 avatar May 14 '25 14:05 sebyx07

hey, do u need any more changes for this to get merged?

sebyx07 avatar May 16 '25 15:05 sebyx07

cc @tarcieri this would be very helpful

andrepcg avatar May 29 '25 11:05 andrepcg

@ixti can you take a look at this one?

tarcieri avatar Jun 14 '25 18:06 tarcieri

@sebyx07 Thank you for the PR, can you please:

  1. rebase it on top of main
  2. remove Ruby-3.4 related fixes (like those s/inspect/to_json/) - we will deal with them later
  3. Use constants instead of magic numbers (e.g. ADDR_IPV4 = 0x01 instead of using value directly with comment), something like:
    module HTTP
      # SOCKS5 proxy implementation
      class SOCKS5Proxy
        ADDR_IPV4   = 0x01
        ADDR_DOMAIN = 0x03
        ADDR_IPV6   = 0x04
    
        IPV4_RE = /\A\d+\.\d+\.\d+\.\d+\z/
    
        ERR_MESAGES = {
          0x01 => "general SOCKS server failure",
          0x02 => "connection not allowed by ruleset",
          0x03 => "Network unreachable",
          0x04 => "Host unreachable",
          0x05 => "Connection refused",
          0x06 => "TTL expired",
          0x07 => "Command not supported",
          0x08 => "Address type not supported"
        }.freeze
    
        # ...
    
        # Handle the reply code from the SOCKS5 proxy
        # @param [Integer] reply The reply code
        # @raise [HTTP::ConnectionError] if the reply indicates an error
        def handle_reply_code(reply)
          return if reply.zero?
    
          @failed_connect = true
          error_message = ERR_MESAGES.fetch(reply, "Unknown error (code: #{reply})")
          raise ConnectionError, "SOCKS5 proxy connection failed: #{error_message}"
        end
    
        # Skip the bound address and port in the response
        # @param [Integer] atyp The address type
        # @return [void]
        def skip_bound_address(atyp)
          case atyp
          when ADDR_IPV4
            @socket.readpartial(6) # 4 bytes for IPv4 + 2 bytes for port
          when ADDR_DOMAIN
            domain_len = @socket.readpartial(1).unpack1("C")
            @socket.readpartial(domain_len + 2) # domain length + 2 bytes for port
          when ADDR_IPV6
            @socket.readpartial(18) # 16 bytes for IPv6 + 2 bytes for port
          end
        end
      end
    end
    

Also, I've noticed that skip_bound_address handles IPV6, but format_address does not, is that intentional?

ixti avatar Jun 20 '25 02:06 ixti

i'm busy with my work right now, you can fork my code and do the required updates, i have my own fork self hosted http gem anyway, i don't like to rely on others for mergex/fixes etc

sebyx07 avatar Jun 25 '25 14:06 sebyx07

i just shared it with you in case you want to add it to the public gem

sebyx07 avatar Jun 25 '25 14:06 sebyx07

@sebyx07 thank you!

ixti avatar Jun 26 '25 17:06 ixti