socksify 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 24bd364bbfc92b361cbd76623e0394476141f9f8
4
- data.tar.gz: 72d2a6761ee659f0b96773048c6f1834d205e7a9
2
+ SHA256:
3
+ metadata.gz: c9c9f42d889fe8506efd5c6d7bbd13484e6dfed208ac5d125cfeb661a7f03d1d
4
+ data.tar.gz: fdb78e9915cb13f17325173d721df354896fb085f82da79a1f7fb2a73b0f8076
5
5
  SHA512:
6
- metadata.gz: 9b9cf647a3dacbf545aad7e89779b34098b624614548572aee91210ca3387320b1b549db82ecebf84d236679a631f6bf87b7ad37883de3ad132427ebc50a98ea
7
- data.tar.gz: 4011606be7484d050d32b37825f3fe5c340557b50c5be0eb3f25588d89ecbe22bc82b7e13833030f860d70e5a20ef7079219e296c24836b117ca4672b02444ca
6
+ metadata.gz: 5b15e3c4c7b72a03386d0a3f78472d659a44cddd5c9f846d57fc97e3c149a5cedde40958ec78de01a3ac4cb0f828f23f5f7b79adfc4c6d66501973e630f8bb2c
7
+ data.tar.gz: 2717aac706890dd74854a02dac6c714aebf3f54d26861144dfa32b6d684965e95d1e1aa4c8f078748fd92d28085a6dbd7495957ea65254bbdbba2bf92e9e8609
data/bin/socksify_ruby CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  if ARGV.size < 2
4
- puts "Usage: #{$0} <SOCKS host> <SOCKS port> [script args ...]"
4
+ puts "Usage: #{$PROGRAM_NAME} <SOCKS host> <SOCKS port> [script args ...]"
5
5
  exit
6
6
  end
7
7
 
@@ -15,4 +15,3 @@ else
15
15
  require 'irb'
16
16
  IRB.start(__FILE__)
17
17
  end
18
-
data/doc/index.html CHANGED
@@ -77,6 +77,12 @@ Socksify::proxy("127.0.0.1", 9050) {
77
77
  </pre>
78
78
  </p>
79
79
 
80
+ <p>
81
+ Please note: <b>socksify is not thread-safe</b> when used this way!
82
+ <code>socks_server</code> and <code>socks_port</code> are stored in class
83
+ <code>@@</code>-variables, and applied to all threads and fibers of application.
84
+ </p>
85
+
80
86
  <h3>Use Net::HTTP explicitly via SOCKS</h3>
81
87
  <p>
82
88
  Require the additional library <code>socksify/http</code>
@@ -99,6 +105,14 @@ end
99
105
  explicitly or use <code>Net::HTTP</code> directly.
100
106
  </p>
101
107
 
108
+ <p>
109
+ <code>Net::HTTP.SOCKSProxy</code> also supports SOCKS authentication:
110
+ </p>
111
+ <pre>
112
+ Net::HTTP.SOCKSProxy('127.0.0.1', 9050, 'username', 'p4ssw0rd')
113
+ </pre>
114
+
115
+
102
116
  <h3>Resolve addresses via SOCKS</h3>
103
117
  <pre>Socksify::resolve("spaceboyz.net")
104
118
  # => "87.106.131.203"</pre>
@@ -1,32 +1,47 @@
1
+ # namespace
1
2
  module Socksify
3
+ # rubocop:disable Style/Documentation
2
4
  class Color
3
5
  class Reset
4
- def self::to_s
6
+ def self.to_s
5
7
  "\e[0m\e[37m"
6
8
  end
7
9
  end
8
-
10
+
9
11
  class Red < Color
10
- def num; 31; end
12
+ def num
13
+ 31
14
+ end
11
15
  end
16
+
12
17
  class Green < Color
13
- def num; 32; end
18
+ def num
19
+ 32
20
+ end
14
21
  end
22
+
15
23
  class Yellow < Color
16
- def num; 33; end
24
+ def num
25
+ 33
26
+ end
17
27
  end
18
-
19
- def self::to_s
28
+ # rubocop:enable Style/Documentation
29
+
30
+ def self.to_s
20
31
  new.to_s
21
32
  end
22
-
33
+
34
+ def num
35
+ 0
36
+ end
37
+
23
38
  def to_s
24
39
  "\e[1m\e[#{num}m"
25
40
  end
26
41
  end
27
42
 
28
43
  def self.debug=(enabled)
29
- @@debug = enabled
44
+ @debug = enabled
30
45
  end
31
46
 
32
47
  def self.debug_debug(str)
@@ -41,12 +56,8 @@ module Socksify
41
56
  debug(Color::Red, str)
42
57
  end
43
58
 
44
- private
45
-
46
59
  def self.debug(color, str)
47
- if defined? @@debug
48
- puts "#{color}#{now_s}#{Color::Reset} #{str}"
49
- end
60
+ puts "#{color}#{now_s}#{Color::Reset} #{str}" if defined?(@debug) && @debug
50
61
  end
51
62
 
52
63
  def self.now_s
data/lib/socksify/http.rb CHANGED
@@ -1,59 +1,64 @@
1
- =begin
2
- Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
3
- Copyright (C) 2011 Musy Bite <musybite@gmail.com>
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU General Public License for more details.
14
-
15
- You should have received a copy of the GNU General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
- =end
1
+ # Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
2
+ # Copyright (C) 2011 Musy Bite <musybite@gmail.com>
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
16
 
19
17
  require 'socksify'
20
18
  require 'net/http'
19
+ require_relative 'ruby3net_http_connectable'
21
20
 
22
21
  module Net
22
+ # patched class
23
23
  class HTTP
24
- def self.SOCKSProxy(p_host, p_port)
25
- delta = SOCKSProxyDelta
26
- proxyclass = Class.new(self)
27
- proxyclass.send(:include, delta)
28
- proxyclass.module_eval {
29
- include delta::InstanceMethods
30
- extend delta::ClassMethods
24
+ def self.socks_proxy(p_host, p_port, username: nil, password: nil)
25
+ proxyclass.module_eval do
26
+ include Ruby3NetHTTPConnectable if RUBY_VERSION.to_f > 3.0 # patch #connect method
27
+ include SOCKSProxyDelta::InstanceMethods
28
+ extend SOCKSProxyDelta::ClassMethods
29
+
31
30
  @socks_server = p_host
32
31
  @socks_port = p_port
33
- }
32
+ @socks_username = username
33
+ @socks_password = password
34
+ end
35
+
34
36
  proxyclass
35
37
  end
36
38
 
39
+ def self.proxyclass
40
+ @proxyclass ||= Class.new(self).tap { |klass| klass.send(:include, SOCKSProxyDelta) }
41
+ end
42
+
43
+ class << self
44
+ alias SOCKSProxy socks_proxy # legacy support for non snake case method name
45
+ end
46
+
37
47
  module SOCKSProxyDelta
48
+ # class methods
38
49
  module ClassMethods
39
- def socks_server
40
- @socks_server
41
- end
42
-
43
- def socks_port
44
- @socks_port
45
- end
50
+ attr_reader :socks_server, :socks_port,
51
+ :socks_username, :socks_password
46
52
  end
47
53
 
54
+ # instance methods - no long supports Ruby < 2
48
55
  module InstanceMethods
49
- if RUBY_VERSION[0..0] >= '2'
50
- def address
51
- TCPSocket::SOCKSConnectionPeerAddress.new(self.class.socks_server, self.class.socks_port, @address)
52
- end
53
- else
54
- def conn_address
55
- TCPSocket::SOCKSConnectionPeerAddress.new(self.class.socks_server, self.class.socks_port, address())
56
- end
56
+ def address
57
+ TCPSocket::SOCKSConnectionPeerAddress.new(
58
+ self.class.socks_server, self.class.socks_port,
59
+ @address,
60
+ self.class.socks_username, self.class.socks_password
61
+ )
57
62
  end
58
63
  end
59
64
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby 3.0 private method Net::HTTP#connect
4
+ module Ruby3NetHTTPConnectable
5
+ # rubocop:disable all - CAN'T LINT RUBY SOURCE CODE!
6
+ def connect
7
+ if use_ssl? # 3.1 - MOVED FROM FURTHER DOWN IN METHOD
8
+ # reference early to load OpenSSL before connecting,
9
+ # as OpenSSL may take time to load.
10
+ @ssl_context = OpenSSL::SSL::SSLContext.new
11
+ end
12
+
13
+ if proxy? then
14
+ conn_addr = proxy_address
15
+ conn_port = proxy_port
16
+ else
17
+ conn_addr = conn_address
18
+ conn_port = port
19
+ end
20
+
21
+ D "opening connection to #{conn_addr}:#{conn_port}..."
22
+ ######## RUBY < 3.1 ########
23
+ s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
24
+ begin
25
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
26
+ rescue => e
27
+ raise e, "Failed to open TCP connection to " +
28
+ "#{conn_addr}:#{conn_port} (#{e.message})"
29
+ end
30
+ }
31
+ ######## END RUBY < 3.1 ########
32
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
33
+ D "opened"
34
+ if use_ssl?
35
+ if proxy?
36
+ plain_sock = Net::BufferedIO.new(s, read_timeout: @read_timeout, # 3.1 - FULLY QUALIFY CLASS
37
+ write_timeout: @write_timeout,
38
+ continue_timeout: @continue_timeout,
39
+ debug_output: @debug_output)
40
+ buf = "CONNECT #{conn_address}:#{@port} HTTP/#{Net::HTTP::HTTPVersion}\r\n" # 3.1 - FULLY QUALIFY CONSTANT
41
+ buf << "Host: #{@address}:#{@port}\r\n"
42
+ if proxy_user
43
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
44
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
45
+ end
46
+ buf << "\r\n"
47
+ plain_sock.write(buf)
48
+ Net::HTTPResponse.read_new(plain_sock).value # 3.1 - FULLY QUALIFY CLASS
49
+ # assuming nothing left in buffers after successful CONNECT response
50
+ end
51
+
52
+ ssl_parameters = Hash.new
53
+ iv_list = instance_variables
54
+ Net::HTTP::SSL_IVNAMES.each_with_index do |ivname, i| # 3.1 - FULLY QUALIFY CONSTANT
55
+ if iv_list.include?(ivname)
56
+ value = instance_variable_get(ivname)
57
+ unless value.nil?
58
+ ssl_parameters[Net::HTTP::SSL_ATTRIBUTES[i]] = value # 3.1 - FULLY QUALIFY CONSTANT
59
+ end
60
+ end
61
+ end
62
+ # @ssl_context = OpenSSL::SSL::SSLContext.new # 3.1 - MOVED TO TOP OF METHOD
63
+ @ssl_context.set_params(ssl_parameters)
64
+ @ssl_context.session_cache_mode =
65
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
66
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
67
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
68
+ D "starting SSL for #{conn_addr}:#{conn_port}..."
69
+ s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
70
+ s.sync_close = true
71
+ # Server Name Indication (SNI) RFC 3546
72
+ s.hostname = @address if s.respond_to? :hostname=
73
+ if @ssl_session and
74
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
75
+ s.session = @ssl_session
76
+ end
77
+ ssl_socket_connect(s, @open_timeout)
78
+ if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && @ssl_context.verify_hostname
79
+ s.post_connection_check(@address)
80
+ end
81
+ D "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
82
+ end
83
+ @socket = Net::BufferedIO.new(s, read_timeout: @read_timeout, # 3.1 - FULLY QUALIFY CLASS
84
+ write_timeout: @write_timeout,
85
+ continue_timeout: @continue_timeout,
86
+ debug_output: @debug_output)
87
+ @last_communicated = nil # 3.1 - NEW
88
+ on_connect
89
+ rescue => exception
90
+ if s
91
+ D "Conn close because of connect error #{exception}"
92
+ s.close
93
+ end
94
+ raise
95
+ end
96
+ # rubocop:enable all
97
+ private :connect
98
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ # decorator methods for socks proxying
4
+ module Socksproxyable
5
+ # class methods
6
+ module ClassMethods
7
+ SOCKS4_VERSIONS = %w[4 4a].freeze
8
+
9
+ attr_accessor :socks_server, :socks_port, :socks_username, :socks_password
10
+
11
+ def socks_version
12
+ @socks_version ||= '5'
13
+ end
14
+
15
+ def socks_ignores
16
+ @socks_ignores ||= %w[localhost]
17
+ end
18
+
19
+ def socks_ignores=(*hosts)
20
+ @socks_ignores = hosts
21
+ end
22
+
23
+ def socks_version_hex
24
+ SOCKS4_VERSIONS.include?(socks_version) ? "\004" : "\005"
25
+ end
26
+ end
27
+
28
+ # instance method #socks_authenticate
29
+ module InstanceMethodsAuthenticate
30
+ # rubocop:disable Metrics
31
+ def socks_authenticate(socks_username, socks_password)
32
+ if socks_username || socks_password
33
+ Socksify.debug_debug 'Sending username/password authentication'
34
+ write "\005\001\002"
35
+ else
36
+ Socksify.debug_debug 'Sending no authentication'
37
+ write "\005\001\000"
38
+ end
39
+ Socksify.debug_debug 'Waiting for authentication reply'
40
+ auth_reply = recv(2)
41
+ raise SOCKSError, "Server doesn't reply authentication" if auth_reply.empty?
42
+
43
+ if auth_reply[0..0] != "\004" && auth_reply[0..0] != "\005"
44
+ raise SOCKSError, "SOCKS version #{auth_reply[0..0]} not supported"
45
+ end
46
+
47
+ if socks_username || socks_password
48
+ if auth_reply[1..1] != "\002"
49
+ raise SOCKSError, "SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported"
50
+ end
51
+
52
+ auth = "\001"
53
+ auth += socks_username.to_s.length.chr
54
+ auth += socks_username.to_s
55
+ auth += socks_password.to_s.length.chr
56
+ auth += socks_password.to_s
57
+ write auth
58
+ auth_reply = recv(2)
59
+ raise SOCKSError, 'SOCKS authentication failed' if auth_reply[1..1] != "\000"
60
+ elsif auth_reply[1..1] != "\000"
61
+ raise SOCKSError, "SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported"
62
+ end
63
+ end
64
+ # rubocop:enable Metrics
65
+ end
66
+
67
+ # instance methods #socks_connect & #socks_receive_reply
68
+ module InstanceMethodsConnect
69
+ # rubocop:disable Metrics
70
+ def socks_connect(host, port)
71
+ port = Socket.getservbyname(port) if port.is_a?(String)
72
+ req = String.new
73
+ Socksify.debug_debug 'Sending destination address'
74
+ req << TCPSocket.socks_version_hex
75
+ Socksify.debug_debug TCPSocket.socks_version_hex.unpack 'H*'
76
+ req << "\001"
77
+ req << "\000" if self.class.socks_version == '5'
78
+ req << [port].pack('n') if self.class.socks_version =~ /^4/
79
+ host = Resolv::DNS.new.getaddress(host).to_s if self.class.socks_version == '4'
80
+ Socksify.debug_debug host
81
+ if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # to IPv4 address
82
+ req << "\001" if self.class.socks_version == '5'
83
+ ip = (1..4).map { |i| Regexp.last_match(i).to_i }.pack('CCCC')
84
+ req << ip
85
+ elsif host =~ /^[:0-9a-f]+$/ # to IPv6 address
86
+ raise 'TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor'
87
+ # req << "\004" # UNREACHABLE
88
+ elsif self.class.socks_version == '5' # to hostname
89
+ # req << "\003" + [host.size].pack('C') + host
90
+ req << "\003#{[host.size].pack('C')}#{host}"
91
+ else
92
+ req << "\000\000\000\001" << "\007\000"
93
+ Socksify.debug_notice host
94
+ req << host << "\000"
95
+ end
96
+ req << [port].pack('n') if self.class.socks_version == '5'
97
+ write req
98
+ socks_receive_reply
99
+ Socksify.debug_notice "Connected to #{host}:#{port} over SOCKS"
100
+ end
101
+ # rubocop:enable Metrics
102
+
103
+ # returns [bind_addr: String, bind_port: Fixnum]
104
+ # rubocop:disable Metrics
105
+ def socks_receive_reply
106
+ Socksify.debug_debug 'Waiting for SOCKS reply'
107
+ if self.class.socks_version == '5'
108
+ connect_reply = recv(4)
109
+ raise SOCKSError, "Server doesn't reply" if connect_reply.empty?
110
+
111
+ Socksify.debug_debug connect_reply.unpack 'H*'
112
+ raise SOCKSError, "SOCKS version #{connect_reply[0..0]} is not 5" if connect_reply[0..0] != "\005"
113
+ raise SOCKSError.for_response_code(connect_reply.bytes.to_a[1]) if connect_reply[1..1] != "\000"
114
+
115
+ Socksify.debug_debug 'Waiting for bind_addr'
116
+ bind_addr_len = case connect_reply[3..3]
117
+ when "\001"
118
+ 4
119
+ when "\003"
120
+ recv(1).bytes.first
121
+ when "\004"
122
+ 16
123
+ else
124
+ raise SOCKSError.for_response_code(connect_reply.bytes.to_a[3])
125
+ end
126
+ bind_addr_s = recv(bind_addr_len)
127
+ bind_addr = case connect_reply[3..3]
128
+ when "\001"
129
+ bind_addr_s.bytes.to_a.join('.')
130
+ when "\003"
131
+ bind_addr_s
132
+ when "\004" # Untested!
133
+ i = 0
134
+ ip6 = ''
135
+ bind_addr_s.each_byte do |b|
136
+ ip6 += ':' if i > 0 && i.even?
137
+ i += 1
138
+ ip6 += b.to_s(16).rjust(2, '0')
139
+ end
140
+ end
141
+ bind_port = recv(bind_addr_len + 2)
142
+ [bind_addr, bind_port.unpack('n')]
143
+ else
144
+ connect_reply = recv(8)
145
+ unless connect_reply[0] == "\000" && connect_reply[1] == "\x5A"
146
+ Socksify.debug_debug connect_reply.unpack 'H'
147
+ raise SOCKSError, 'Failed while connecting througth socks'
148
+ end
149
+ end
150
+ end
151
+ # rubocop:enable Metrics
152
+ end
153
+ end
@@ -0,0 +1,82 @@
1
+ require_relative 'socksproxyable'
2
+
3
+ # monkey patch
4
+ class TCPSocket
5
+ extend Socksproxyable::ClassMethods
6
+ include Socksproxyable::InstanceMethodsAuthenticate
7
+ include Socksproxyable::InstanceMethodsConnect
8
+
9
+ alias initialize_tcp initialize
10
+
11
+ attr_reader :socks_peer
12
+
13
+ # See http://tools.ietf.org/html/rfc1928
14
+ # rubocop:disable Metrics/ParameterLists
15
+ def initialize(host = nil, port = nil, local_host = nil, local_port = nil, **kwargs)
16
+ @socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress)
17
+ host = socks_peer.peer_host if socks_peer
18
+
19
+ if socks_server && socks_port && !socks_ignores.include?(host)
20
+ make_socks_connection(host, port, **kwargs)
21
+ else
22
+ make_direct_connection(host, port, local_host, local_port, **kwargs)
23
+ end
24
+ end
25
+ # rubocop:enable Metrics/ParameterLists
26
+
27
+ # string representation of the peer host address
28
+ class SOCKSConnectionPeerAddress < String
29
+ attr_reader :socks_server, :socks_port, :socks_username, :socks_password
30
+
31
+ def initialize(socks_server, socks_port, peer_host, socks_username = nil, socks_password = nil)
32
+ @socks_server = socks_server
33
+ @socks_port = socks_port
34
+ @socks_username = socks_username
35
+ @socks_password = socks_password
36
+ super(peer_host)
37
+ end
38
+
39
+ def inspect
40
+ "#{self} (via #{@socks_server}:#{@socks_port})"
41
+ end
42
+
43
+ def peer_host
44
+ to_s
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def socks_server
51
+ @socks_server ||= socks_peer ? socks_peer.socks_server : self.class.socks_server
52
+ end
53
+
54
+ def socks_port
55
+ @socks_port ||= socks_peer ? socks_peer.socks_port : self.class.socks_port
56
+ end
57
+
58
+ def socks_username
59
+ @socks_username ||= socks_peer ? socks_peer.socks_username : self.class.socks_username
60
+ end
61
+
62
+ def socks_password
63
+ @socks_password ||= socks_peer ? socks_peer.socks_password : self.class.socks_password
64
+ end
65
+
66
+ def socks_ignores
67
+ @socks_ignores ||= socks_peer ? [] : self.class.socks_ignores
68
+ end
69
+
70
+ def make_socks_connection(host, port, **kwargs)
71
+ Socksify.debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}"
72
+ initialize_tcp socks_server, socks_port, **kwargs
73
+ socks_authenticate(socks_username, socks_password) unless @socks_version =~ /^4/
74
+ socks_connect(host, port) if host
75
+ end
76
+
77
+ def make_direct_connection(host, port, local_host, local_port, **kwargs)
78
+ Socksify.debug_notice "Connecting directly to #{host}:#{port}"
79
+ initialize_tcp host, port, local_host, local_port, **kwargs
80
+ Socksify.debug_debug "Connected to #{host}:#{port}"
81
+ end
82
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # namespace
4
+ module Socksify
5
+ VERSION = '1.8.0'
6
+ end
data/lib/socksify.rb CHANGED
@@ -1,364 +1,134 @@
1
- #encoding: us-ascii
2
- =begin
3
- Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU General Public License for more details.
14
-
15
- You should have received a copy of the GNU General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
- =end
1
+ # encoding: us-ascii
2
+
3
+ # Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
17
 
19
18
  require 'socket'
20
19
  require 'resolv'
21
20
  require 'socksify/debug'
21
+ require 'socksify/tcpsocket'
22
22
 
23
+ # error class
23
24
  class SOCKSError < RuntimeError
24
25
  def initialize(msg)
25
- Socksify::debug_error("#{self.class}: #{msg}")
26
+ Socksify.debug_error("#{self.class}: #{msg}")
26
27
  super
27
28
  end
28
29
 
30
+ # rubocop:disable Style/Documentation
29
31
  class ServerFailure < SOCKSError
30
32
  def initialize
31
- super("general SOCKS server failure")
33
+ super('general SOCKS server failure')
32
34
  end
33
35
  end
36
+
34
37
  class NotAllowed < SOCKSError
35
38
  def initialize
36
- super("connection not allowed by ruleset")
39
+ super('connection not allowed by ruleset')
37
40
  end
38
41
  end
42
+
39
43
  class NetworkUnreachable < SOCKSError
40
44
  def initialize
41
- super("Network unreachable")
45
+ super('Network unreachable')
42
46
  end
43
47
  end
48
+
44
49
  class HostUnreachable < SOCKSError
45
50
  def initialize
46
- super("Host unreachable")
51
+ super('Host unreachable')
47
52
  end
48
53
  end
54
+
49
55
  class ConnectionRefused < SOCKSError
50
56
  def initialize
51
- super("Connection refused")
57
+ super('Connection refused')
52
58
  end
53
59
  end
60
+
54
61
  class TTLExpired < SOCKSError
55
62
  def initialize
56
- super("TTL expired")
63
+ super('TTL expired')
57
64
  end
58
65
  end
66
+
59
67
  class CommandNotSupported < SOCKSError
60
68
  def initialize
61
- super("Command not supported")
69
+ super('Command not supported')
62
70
  end
63
71
  end
72
+
64
73
  class AddressTypeNotSupported < SOCKSError
65
74
  def initialize
66
- super("Address type not supported")
67
- end
68
- end
69
-
70
- def self.for_response_code(code)
71
- case code
72
- when 1
73
- ServerFailure
74
- when 2
75
- NotAllowed
76
- when 3
77
- NetworkUnreachable
78
- when 4
79
- HostUnreachable
80
- when 5
81
- ConnectionRefused
82
- when 6
83
- TTLExpired
84
- when 7
85
- CommandNotSupported
86
- when 8
87
- AddressTypeNotSupported
88
- else
89
- self
90
- end
91
- end
92
- end
93
-
94
- class TCPSocket
95
- @@socks_version ||= "5"
96
-
97
- def self.socks_version
98
- (@@socks_version == "4a" or @@socks_version == "4") ? "\004" : "\005"
99
- end
100
- def self.socks_version=(version)
101
- @@socks_version = version.to_s
102
- end
103
- def self.socks_server
104
- @@socks_server ||= nil
105
- end
106
- def self.socks_server=(host)
107
- @@socks_server = host
108
- end
109
- def self.socks_port
110
- @@socks_port ||= nil
111
- end
112
- def self.socks_port=(port)
113
- @@socks_port = port
114
- end
115
- def self.socks_username
116
- @@socks_username ||= nil
117
- end
118
- def self.socks_username=(username)
119
- @@socks_username = username
120
- end
121
- def self.socks_password
122
- @@socks_password ||= nil
123
- end
124
- def self.socks_password=(password)
125
- @@socks_password = password
126
- end
127
- def self.socks_ignores
128
- @@socks_ignores ||= %w(localhost)
129
- end
130
- def self.socks_ignores=(ignores)
131
- @@socks_ignores = ignores
132
- end
133
-
134
- class SOCKSConnectionPeerAddress < String
135
- attr_reader :socks_server, :socks_port
136
-
137
- def initialize(socks_server, socks_port, peer_host)
138
- @socks_server, @socks_port = socks_server, socks_port
139
- super peer_host
75
+ super('Address type not supported')
140
76
  end
141
-
142
- def inspect
143
- "#{to_s} (via #{@socks_server}:#{@socks_port})"
144
- end
145
-
146
- def peer_host
147
- to_s
148
- end
149
- end
150
-
151
- alias :initialize_tcp :initialize
152
-
153
- # See http://tools.ietf.org/html/rfc1928
154
- def initialize(host=nil, port=0, local_host=nil, local_port=nil)
155
- if host.is_a?(SOCKSConnectionPeerAddress)
156
- socks_peer = host
157
- socks_server = socks_peer.socks_server
158
- socks_port = socks_peer.socks_port
159
- socks_ignores = []
160
- host = socks_peer.peer_host
161
- else
162
- socks_server = self.class.socks_server
163
- socks_port = self.class.socks_port
164
- socks_ignores = self.class.socks_ignores
165
- end
166
-
167
- if socks_server and socks_port and not socks_ignores.include?(host)
168
- Socksify::debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}"
169
- initialize_tcp socks_server, socks_port
170
-
171
- socks_authenticate unless @@socks_version =~ /^4/
172
-
173
- if host
174
- socks_connect(host, port)
175
- end
176
- else
177
- Socksify::debug_notice "Connecting directly to #{host}:#{port}"
178
- initialize_tcp host, port, local_host, local_port
179
- Socksify::debug_debug "Connected to #{host}:#{port}"
180
- end
181
- end
182
-
183
- # Authentication
184
- def socks_authenticate
185
- if self.class.socks_username || self.class.socks_password
186
- Socksify::debug_debug "Sending username/password authentication"
187
- write "\005\001\002"
188
- else
189
- Socksify::debug_debug "Sending no authentication"
190
- write "\005\001\000"
191
- end
192
- Socksify::debug_debug "Waiting for authentication reply"
193
- auth_reply = recv(2)
194
- if auth_reply.empty?
195
- raise SOCKSError.new("Server doesn't reply authentication")
196
- end
197
- if auth_reply[0..0] != "\004" and auth_reply[0..0] != "\005"
198
- raise SOCKSError.new("SOCKS version #{auth_reply[0..0]} not supported")
199
- end
200
- if self.class.socks_username || self.class.socks_password
201
- if auth_reply[1..1] != "\002"
202
- raise SOCKSError.new("SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported")
203
- end
204
- auth = "\001"
205
- auth += self.class.socks_username.to_s.length.chr
206
- auth += self.class.socks_username.to_s
207
- auth += self.class.socks_password.to_s.length.chr
208
- auth += self.class.socks_password.to_s
209
- write auth
210
- auth_reply = recv(2)
211
- if auth_reply[1..1] != "\000"
212
- raise SOCKSError.new("SOCKS authentication failed")
213
- end
214
- else
215
- if auth_reply[1..1] != "\000"
216
- raise SOCKSError.new("SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported")
217
- end
218
- end
219
- end
220
-
221
- # Connect
222
- def socks_connect(host, port)
223
- port = Socket.getservbyname(port) if port.is_a?(String)
224
- req = String.new
225
- Socksify::debug_debug "Sending destination address"
226
- req << TCPSocket.socks_version
227
- Socksify::debug_debug TCPSocket.socks_version.unpack "H*"
228
- req << "\001"
229
- req << "\000" if @@socks_version == "5"
230
- req << [port].pack('n') if @@socks_version =~ /^4/
231
-
232
- if @@socks_version == "4"
233
- host = Resolv::DNS.new.getaddress(host).to_s
234
- end
235
- Socksify::debug_debug host
236
- if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # to IPv4 address
237
- req << "\001" if @@socks_version == "5"
238
- _ip = [$1.to_i,
239
- $2.to_i,
240
- $3.to_i,
241
- $4.to_i
242
- ].pack('CCCC')
243
- req << _ip
244
- elsif host =~ /^[:0-9a-f]+$/ # to IPv6 address
245
- raise "TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor"
246
- req << "\004"
247
- else # to hostname
248
- if @@socks_version == "5"
249
- req << "\003" + [host.size].pack('C') + host
250
- else
251
- req << "\000\000\000\001"
252
- req << "\007\000"
253
- Socksify::debug_notice host
254
- req << host
255
- req << "\000"
256
- end
257
- end
258
- req << [port].pack('n') if @@socks_version == "5"
259
- write req
260
-
261
- socks_receive_reply
262
- Socksify::debug_notice "Connected to #{host}:#{port} over SOCKS"
263
77
  end
78
+ # rubocop:enable Style/Documentation
264
79
 
265
- # returns [bind_addr: String, bind_port: Fixnum]
266
- def socks_receive_reply
267
- Socksify::debug_debug "Waiting for SOCKS reply"
268
- if @@socks_version == "5"
269
- connect_reply = recv(4)
270
- if connect_reply.empty?
271
- raise SOCKSError.new("Server doesn't reply")
272
- end
273
- Socksify::debug_debug connect_reply.unpack "H*"
274
- if connect_reply[0..0] != "\005"
275
- raise SOCKSError.new("SOCKS version #{connect_reply[0..0]} is not 5")
276
- end
277
- if connect_reply[1..1] != "\000"
278
- raise SOCKSError.for_response_code(connect_reply.bytes.to_a[1])
279
- end
280
- Socksify::debug_debug "Waiting for bind_addr"
281
- bind_addr_len = case connect_reply[3..3]
282
- when "\001"
283
- 4
284
- when "\003"
285
- recv(1).bytes.first
286
- when "\004"
287
- 16
288
- else
289
- raise SOCKSError.for_response_code(connect_reply.bytes.to_a[3])
290
- end
291
- bind_addr_s = recv(bind_addr_len)
292
- bind_addr = case connect_reply[3..3]
293
- when "\001"
294
- bind_addr_s.bytes.to_a.join('.')
295
- when "\003"
296
- bind_addr_s
297
- when "\004" # Untested!
298
- i = 0
299
- ip6 = ""
300
- bind_addr_s.each_byte do |b|
301
- if i > 0 and i % 2 == 0
302
- ip6 += ":"
303
- end
304
- i += 1
80
+ RESPONSE_CODE_CLASSES = { 1 => ServerFailure,
81
+ 2 => NotAllowed,
82
+ 3 => NetworkUnreachable,
83
+ 4 => HostUnreachable,
84
+ 5 => ConnectionRefused,
85
+ 6 => TTLExpired,
86
+ 7 => CommandNotSupported,
87
+ 8 => AddressTypeNotSupported }.freeze
305
88
 
306
- ip6 += b.to_s(16).rjust(2, '0')
307
- end
308
- end
309
- bind_port = recv(bind_addr_len + 2)
310
- [bind_addr, bind_port.unpack('n')]
311
- else
312
- connect_reply = recv(8)
313
- unless connect_reply[0] == "\000" and connect_reply[1] == "\x5A"
314
- Socksify::debug_debug connect_reply.unpack 'H'
315
- raise SOCKSError.new("Failed while connecting througth socks")
316
- end
317
- end
89
+ def self.for_response_code(code)
90
+ (resp = RESPONSE_CODE_CLASSES[code]) ? resp : self
318
91
  end
319
92
  end
320
93
 
94
+ # namespace
321
95
  module Socksify
322
96
  def self.resolve(host)
323
- s = TCPSocket.new
324
-
325
- begin
326
- req = String.new
327
- Socksify::debug_debug "Sending hostname to resolve: #{host}"
328
- req << "\005"
329
- if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # to IPv4 address
330
- req << "\xF1\000\001" + [$1.to_i,
331
- $2.to_i,
332
- $3.to_i,
333
- $4.to_i
334
- ].pack('CCCC')
335
- elsif host =~ /^[:0-9a-f]+$/ # to IPv6 address
336
- raise "TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor"
337
- req << "\004"
338
- else # to hostname
339
- req << "\xF0\000\003" + [host.size].pack('C') + host
340
- end
341
- req << [0].pack('n') # Port
342
- s.write req
343
-
344
- addr, _port = s.socks_receive_reply
345
- Socksify::debug_notice "Resolved #{host} as #{addr} over SOCKS"
346
- addr
347
- ensure
348
- s.close
349
- end
97
+ socket = TCPSocket.new # no args?
98
+ Socksify.debug_debug "Sending hostname to resolve: #{host}"
99
+ req = request(host)
100
+ socket.write req
101
+ addr, _port = socket.socks_receive_reply
102
+ Socksify.debug_notice "Resolved #{host} as #{addr} over SOCKS"
103
+ addr
104
+ ensure
105
+ socket.close
350
106
  end
351
107
 
352
108
  def self.proxy(server, port)
353
- default_server = TCPSocket::socks_server
354
- default_port = TCPSocket::socks_port
109
+ default_server = TCPSocket.socks_server
110
+ default_port = TCPSocket.socks_port
355
111
  begin
356
- TCPSocket::socks_server = server
357
- TCPSocket::socks_port = port
112
+ TCPSocket.socks_server = server
113
+ TCPSocket.socks_port = port
358
114
  yield
359
- ensure
360
- TCPSocket::socks_server = default_server
361
- TCPSocket::socks_port = default_port
115
+ ensure # failback
116
+ TCPSocket.socks_server = default_server
117
+ TCPSocket.socks_port = default_port
118
+ end
119
+ end
120
+
121
+ def self.request(host)
122
+ req = String.new << "\005"
123
+ case host
124
+ when /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # to IPv4 address
125
+ req << "\xF1\000\001#{(1..4).map { |i| Regexp.last_match(i).to_i }.pack('CCCC')}"
126
+ when /^[:0-9a-f]+$/ # to IPv6 address
127
+ raise 'TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor)'
128
+ # # req << "\004" # UNREACHABLE
129
+ else # to hostname
130
+ req << "\xF0\000\003#{[host.size].pack('C')}#{host}"
362
131
  end
132
+ req << [0].pack('n') # Port
363
133
  end
364
134
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: socksify
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.1
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephan Maka
@@ -10,12 +10,10 @@ authors:
10
10
  - Musy Bite
11
11
  - Yuichi Tateno
12
12
  - David Dollar
13
- autorequire:
14
13
  bindir: bin
15
14
  cert_chain: []
16
- date: 2017-02-24 00:00:00.000000000 Z
15
+ date: 2025-07-17 00:00:00.000000000 Z
17
16
  dependencies: []
18
- description:
19
17
  email: stephan@spaceboyz.net
20
18
  executables:
21
19
  - socksify_ruby
@@ -32,12 +30,16 @@ files:
32
30
  - lib/socksify.rb
33
31
  - lib/socksify/debug.rb
34
32
  - lib/socksify/http.rb
35
- homepage: http://socksify.rubyforge.org/
33
+ - lib/socksify/ruby3net_http_connectable.rb
34
+ - lib/socksify/socksproxyable.rb
35
+ - lib/socksify/tcpsocket.rb
36
+ - lib/socksify/version.rb
37
+ homepage: https://github.com/astro/socksify-ruby
36
38
  licenses:
37
39
  - Ruby
38
- - GPL-3.0
39
- metadata: {}
40
- post_install_message:
40
+ - GPL-3.0-only
41
+ metadata:
42
+ funding_uri: https://github.com/sponsors/astro
41
43
  rdoc_options: []
42
44
  require_paths:
43
45
  - lib
@@ -45,16 +47,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
45
47
  requirements:
46
48
  - - ">="
47
49
  - !ruby/object:Gem::Version
48
- version: '0'
50
+ version: '2.0'
49
51
  required_rubygems_version: !ruby/object:Gem::Requirement
50
52
  requirements:
51
53
  - - ">="
52
54
  - !ruby/object:Gem::Version
53
55
  version: '0'
54
56
  requirements: []
55
- rubyforge_project: socksify
56
- rubygems_version: 2.5.2
57
- signing_key:
57
+ rubygems_version: 3.6.6
58
58
  specification_version: 4
59
59
  summary: Redirect all TCPSockets through a SOCKS5 proxy
60
60
  test_files: []