lib/sockit.rb in sockit-0.0.1 vs lib/sockit.rb in sockit-0.0.2

- old
+ new

@@ -1,5 +1,346 @@ +require "socket" +require "resolv" + require "sockit/version" +class SockitError < RuntimeError; end + module Sockit - # Your code goes here... + DEFAULT_CONFIG = { + :version => 5, + :ignore => ["127.0.0.1"], + :debug => false + } + + COLORS = { + :reset => "\e[0m\e[37m", + :red => "\e[1m\e[31m", + :green => "\e[1m\e[32m", + :yellow => "\e[1m\e[33m" + } + + class << self + + def debug(color, message) + timestamp = Time.now.utc + puts("%s%s.%06d %s%s" % [COLORS[color], timestamp.strftime("%Y-%m-%d|%H:%M:%S"), timestamp.usec, message, COLORS[:reset]]) + end + + def dump(action, data) + bytes = Array.new + chars = Array.new + for x in 0..(data.length - 1) do + bytes << ("%03d" % data[x].ord) + chars << ("%03s" % (data[x] =~ /^\w+$/ ? data[x].chr : "...")) + end + debug(:red, "#{action.to_s.upcase}: #{bytes.join(" ")}#{COLORS[:reset]}") + debug(:red, "#{action.to_s.upcase}: #{chars.join(" ")}#{COLORS[:reset]}") + end + + # 0x00 = request granted + # 0x01 = general failure + # 0x02 = connection not allowed by ruleset + # 0x03 = network unreachable + # 0x04 = host unreachable + # 0x05 = connection refused by destination host + # 0x06 = TTL expired + # 0x07 = command not supported / protocol error + # 0x08 = address type not supported + def status_message(status_code) + case status_code + when 0x00 then + "Request granted (Code: 0x%02X)" % status_code + when 0x01 then + "General failure (Code: 0x%02X)" % status_code + when 0x02 then + "Connection not allowed by ruleset (Code: 0x%02X)" % status_code + when 0x03 then + "Network unreachable (Code: 0x%02X)" % status_code + when 0x04 then + "Host unreachable (Code: 0x%02X)" % status_code + when 0x05 then + "Connection refused by destination host (Code: 0x%02X)" % status_code + when 0x06 then + "TTL expired (Code: 0x%02X)" % status_code + when 0x07 then + "Command not supported / Protocol error (Code: 0x%02X)" % status_code + when 0x08 then + "Address type not supported (Code: 0x%02X)" % status_code + else + "Unknown (Code: 0x%02X)" % status_code + end + end + + # The authentication methods supported are numbered as follows: + # 0x00: No authentication + # 0x01: GSSAPI[10] + # 0x02: Username/Password[11] + # 0x03-0x7F: methods assigned by IANA[12] + # 0x80-0xFE: methods reserved for private use + def authentication_method(auth_method) + case auth_method + when 0x00 then + "No authentication (Code: 0x%02X)" % auth_method + when 0x01 then + "GSSAPI authentication (Code: 0x%02X)" % auth_method + when 0x02 then + "Username/Password authentication (Code: 0x%02X)" % auth_method + when 0x03..0x7F then + "Method assigned by IANA (Code: 0x%02X)" % auth_method + when 0x80..0xFE then + "Method reserved for private use (Code: 0x%02X)" % auth_method + when 0xFF then + "Unsupported (Code: 0x%02X)" % auth_method + else + "Unknown (Code: 0x%02X)" % auth_method + end + end + + # 0x00 = success + # any other value = failure, connection must be closed + def authentication_status(auth_status) + case auth_status + when 0x00 then + "Authentication success (Code: 0x%02X)" % auth_status + else + "Authentication failure (Code: 0x%02X)" % auth_status + end + end + + end +end + +class TCPSocket + + class << self + + def socks(&block) + @@socks ||= OpenStruct.new(Sockit::DEFAULT_CONFIG) + if block_given? + yield(@@socks) + else + @@socks + end + end + + end + + def socks(&block) + @@socks ||= OpenStruct.new(Sockit::DEFAULT_CONFIG) + if block_given? + yield(@@socks) + else + @@socks + end + end + + alias :initialize_tcp :initialize + def initialize(remote_host, remote_port, local_host=nil, local_port=nil) + if (socks.host && socks.port && !socks.ignore.include?(remote_host)) + Sockit.debug(:yellow, "Connecting to SOCKS server #{socks.host}:#{socks.port}") + initialize_tcp(socks.host, socks.port) + + (socks.version.to_i == 5) and socks_authenticate + socks.host and socks_connect(remote_host, remote_port) + Sockit.debug(:green, "Connected to #{remote_host}:#{remote_port} via SOCKS server #{socks.host}:#{socks.port}") + else + Sockit.debug(:yellow, "Directly connecting to #{remote_host}:#{remote_port}") + initialize_tcp(remote_host, remote_port, local_host, local_port) + Sockit.debug(:green, "Connected to #{remote_host}:#{remote_port}") + end + end + + def socks_authenticate + # The authentication methods supported are numbered as follows: + # 0x00: No authentication + # 0x01: GSSAPI[10] + # 0x02: Username/Password[11] + # 0x03-0x7F: methods assigned by IANA[12] + # 0x80-0xFE: methods reserved for private use + + # The initial greeting from the client is + # field 1: SOCKS version number (must be 0x05 for this version) + # field 2: number of authentication methods supported, 1 byte + # field 3: authentication methods, variable length, 1 byte per method supported + if (socks.username || socks.password) + data = Array.new + data << [socks.version, 0x02, 0x02, 0x00].pack("C*") + data = data.flatten.join + + socks.debug and Sockit.debug(:yellow, "Requesting username/password authentication") + socks.debug and Sockit.dump(:write, data) + write(data) + else + data = Array.new + data << [socks.version, 0x01, 0x00].pack("C*") + data = data.flatten.join + + socks.debug and Sockit.debug(:yellow, "Requesting no authentication") + socks.debug and Sockit.dump(:write, data) + write(data) + end + + # The server's choice is communicated: + # field 1: SOCKS version, 1 byte (0x05 for this version) + # field 2: chosen authentication method, 1 byte, or 0xFF if no acceptable methods were offered + socks.debug and Sockit.debug(:yellow, "Waiting for SOCKS authentication reply") + auth_reply = recv(2).unpack("C*") + socks.debug and Sockit.dump(:read, auth_reply) + server_socks_version = auth_reply[0] + server_auth_method = auth_reply[1] + + if server_socks_version != socks.version + raise SockitError, "SOCKS server does not support version #{socks.version}!" + end + + if server_auth_method == 0xFF + raise SockitError, Sockit.authentication_method(server_auth_method) + else + socks.debug and Sockit.debug(:green, Sockit.authentication_method(server_auth_method)) + end + + # The subsequent authentication is method-dependent. Username and password authentication (method 0x02) is described in RFC 1929: + case server_auth_method + when 0x00 then + # No authentication + when 0x01 then + # GSSAPI + raise SockitError, "Authentication method GSSAPI not implemented" + when 0x02 then + # For username/password authentication the client's authentication request is + # field 1: version number, 1 byte (must be 0x01) + # field 2: username length, 1 byte + # field 3: username + # field 4: password length, 1 byte + # field 5: password + data = Array.new + data << [0x01].pack("C*") + data << [socks.username.length.to_i].pack("C*") + data << socks.username + data << [socks.password.length.to_i].pack("C*") + data << socks.password + data = data.flatten.join + + socks.debug and Sockit.debug(:yellow, "Sending username and password") + socks.debug and Sockit.dump(:write, data) + write(data) + + # Server response for username/password authentication: + # field 1: version, 1 byte + # field 2: status code, 1 byte. + # 0x00 = success + # any other value = failure, connection must be closed + socks.debug and Sockit.debug(:yellow, "Waiting for SOCKS authentication reply") + auth_reply = recv(2).unpack("C*") + socks.debug and Sockit.dump(:read, auth_reply) + version = auth_reply[0] + status_code = auth_reply[1] + + if status_code == 0x00 + socks.debug and Sockit.debug(:green, Sockit.authentication_status(status_code)) + else + raise SockitError, Sockit.authentication_status(status_code) + end + end + + end + + def socks_connect(remote_host, remote_port) + # The client's connection request is + # field 1: SOCKS version number, 1 byte (must be 0x05 for this version) + # field 2: command code, 1 byte: + # 0x01 = establish a TCP/IP stream connection + # 0x02 = establish a TCP/IP port binding + # 0x03 = associate a UDP port + # field 3: reserved, must be 0x00 + # field 4: address type, 1 byte: + # 0x01 = IPv4 address + # 0x03 = Domain name + # 0x04 = IPv6 address + # field 5: destination address of + # 4 bytes for IPv4 address + # 1 byte of name length followed by the name for Domain name + # 16 bytes for IPv6 address + # field 6: port number in a network byte order, 2 bytes + data = Array.new + data << [ socks.version.to_i, 0x01, 0x00 ].pack("C*") + + # when doing proxy mode on SS5; we seemingly need to resolve all names first. + if remote_host !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ + remote_host = Resolv::DNS.new.getaddress(remote_host).to_s + end + + if remote_host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ + data << [0x01].pack("C*") + data << [$1.to_i, $2.to_i, $3.to_i, $4.to_i].pack("C*") + elsif remote_host =~ /^[:0-9a-f]+$/ + data << [0x04].pack("C*") + data << [$1].pack("C*") + else + data << [0x03].pack("C*") + data << [remote_host.length.to_i].pack("C*") + data << remote_host + end + data << [remote_port].pack("n") + data = data.flatten.join + + Sockit.debug(:yellow, "Requesting SOCKS connection to #{remote_host}:#{remote_port}") + socks.debug and Sockit.dump(:write, data) + write(data) + + # Server response: + # field 1: SOCKS protocol version, 1 byte (0x05 for this version) + # field 2: status, 1 byte: + # 0x00 = request granted + # 0x01 = general failure + # 0x02 = connection not allowed by ruleset + # 0x03 = network unreachable + # 0x04 = host unreachable + # 0x05 = connection refused by destination host + # 0x06 = TTL expired + # 0x07 = command not supported / protocol error + # 0x08 = address type not supported + # field 3: reserved, must be 0x00 + # field 4: address type, 1 byte: + # 0x01 = IPv4 address + # 0x03 = Domain name + # 0x04 = IPv6 address + # field 5: destination address of + # 4 bytes for IPv4 address + # 1 byte of name length followed by the name for Domain name + # 16 bytes for IPv6 address + # field 6: network byte order port number, 2 bytes + socks.debug and Sockit.debug(:yellow, "Waiting for SOCKS connection reply") + packet = recv(4).unpack("C*") + socks.debug and Sockit.dump(:read, packet) + socks_version = packet[0] + status_code = packet[1] + reserved = packet[2] + address_type = packet[3] + + if status_code == 0x00 + socks.debug and Sockit.debug(:green, Sockit.status_message(status_code)) + else + raise SockitError, Sockit.status_message(status_code) + end + + address_length = case address_type + when 0x01 then + 4 + when 0x03 then + data = recv(1).unpack("C*") + socks.debug and Sockit.dump(:read, data) + data[0] + when 0x04 then + 16 + end + address = recv(address_length).unpack("C*") + socks.debug and Sockit.dump(:read, address) + + port = recv(2).unpack("n") + socks.debug and Sockit.dump(:read, port) + + socks.debug and Sockit.debug(:green, [address, port].inspect) + end + end