socksify 1.7.1 → 1.8.1

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: 78b04ded20de21f0dc44eedde301fab64de043f70a24c5108b64f36d443d57d5
4
+ data.tar.gz: 9a704c1a8e8fa005516ca0f09017b340e5286cf48b2c2ce850eea33f9d553dfa
5
5
  SHA512:
6
- metadata.gz: 9b9cf647a3dacbf545aad7e89779b34098b624614548572aee91210ca3387320b1b549db82ecebf84d236679a631f6bf87b7ad37883de3ad132427ebc50a98ea
7
- data.tar.gz: 4011606be7484d050d32b37825f3fe5c340557b50c5be0eb3f25588d89ecbe22bc82b7e13833030f860d70e5a20ef7079219e296c24836b117ca4672b02444ca
6
+ metadata.gz: 75687e942bbf189206db0b531194f2bb6386c5ade89bd80acbe98e9aa0813547506f75900442849f6854c509807f7ab0de733b4cd9380699ea5efd9af61bda59
7
+ data.tar.gz: 2902015d0342ab1ef74fcfb8f20760a0c4a2e8128128448f60794614ecbd8e4de0a21465d371f6b2d5e741a6bd423cd4dd272f6effa18a112085bae55b496d61
data/bin/socksify_ruby CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  if ARGV.size < 2
4
- puts "Usage: #{$0} <SOCKS host> <SOCKS port> [script args ...]"
5
+ puts "Usage: #{$PROGRAM_NAME} <SOCKS host> <SOCKS port> [script args ...]"
5
6
  exit
6
7
  end
7
8
 
@@ -15,4 +16,3 @@ else
15
16
  require 'irb'
16
17
  IRB.start(__FILE__)
17
18
  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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # namespace
1
4
  module Socksify
5
+ # rubocop:disable Style/Documentation
2
6
  class Color
3
7
  class Reset
4
- def self::to_s
8
+ def self.to_s
5
9
  "\e[0m\e[37m"
6
10
  end
7
11
  end
8
-
12
+
9
13
  class Red < Color
10
- def num; 31; end
14
+ def num
15
+ 31
16
+ end
11
17
  end
18
+
12
19
  class Green < Color
13
- def num; 32; end
20
+ def num
21
+ 32
22
+ end
14
23
  end
24
+
15
25
  class Yellow < Color
16
- def num; 33; end
26
+ def num
27
+ 33
28
+ end
17
29
  end
18
-
19
- def self::to_s
30
+ # rubocop:enable Style/Documentation
31
+
32
+ def self.to_s
20
33
  new.to_s
21
34
  end
22
-
35
+
36
+ def num
37
+ 0
38
+ end
39
+
23
40
  def to_s
24
41
  "\e[1m\e[#{num}m"
25
42
  end
26
43
  end
27
44
 
28
45
  def self.debug=(enabled)
29
- @@debug = enabled
46
+ @debug = enabled
30
47
  end
31
48
 
32
49
  def self.debug_debug(str)
@@ -41,12 +58,8 @@ module Socksify
41
58
  debug(Color::Red, str)
42
59
  end
43
60
 
44
- private
45
-
46
61
  def self.debug(color, str)
47
- if defined? @@debug
48
- puts "#{color}#{now_s}#{Color::Reset} #{str}"
49
- end
62
+ puts "#{color}#{now_s}#{Color::Reset} #{str}" if defined?(@debug) && @debug
50
63
  end
51
64
 
52
65
  def self.now_s
data/lib/socksify/http.rb CHANGED
@@ -1,59 +1,66 @@
1
- =begin
2
- Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
3
- Copyright (C) 2011 Musy Bite <musybite@gmail.com>
1
+ # frozen_string_literal: true
4
2
 
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
3
+ # Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
4
+ # Copyright (C) 2011 Musy Bite <musybite@gmail.com>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
18
 
19
19
  require 'socksify'
20
20
  require 'net/http'
21
+ require_relative 'ruby3net_http_connectable'
21
22
 
22
23
  module Net
24
+ # patched class
23
25
  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
26
+ def self.socks_proxy(p_host, p_port, username: nil, password: nil)
27
+ proxyclass.module_eval do
28
+ include Ruby3NetHTTPConnectable if RUBY_VERSION.to_f > 3.0 # patch #connect method
29
+ include SOCKSProxyDelta::InstanceMethods
30
+ extend SOCKSProxyDelta::ClassMethods
31
+
31
32
  @socks_server = p_host
32
33
  @socks_port = p_port
33
- }
34
+ @socks_username = username
35
+ @socks_password = password
36
+ end
37
+
34
38
  proxyclass
35
39
  end
36
40
 
41
+ def self.proxyclass
42
+ @proxyclass ||= Class.new(self).tap { |klass| klass.send(:include, SOCKSProxyDelta) }
43
+ end
44
+
45
+ class << self
46
+ alias SOCKSProxy socks_proxy # legacy support for non snake case method name
47
+ end
48
+
37
49
  module SOCKSProxyDelta
50
+ # class methods
38
51
  module ClassMethods
39
- def socks_server
40
- @socks_server
41
- end
42
-
43
- def socks_port
44
- @socks_port
45
- end
52
+ attr_reader :socks_server, :socks_port,
53
+ :socks_username, :socks_password
46
54
  end
47
55
 
56
+ # instance methods - no long supports Ruby < 2
48
57
  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
58
+ def address
59
+ TCPSocket::SOCKSConnectionPeerAddress.new(
60
+ self.class.socks_server, self.class.socks_port,
61
+ @address,
62
+ self.class.socks_username, self.class.socks_password
63
+ )
57
64
  end
58
65
  end
59
66
  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 = +''
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 /^4/.match?(self.class.socks_version)
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 /^[:0-9a-f]+$/.match?(host) # 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.positive? && 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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'socksproxyable'
4
+
5
+ # monkey patch
6
+ class TCPSocket
7
+ extend Socksproxyable::ClassMethods
8
+ include Socksproxyable::InstanceMethodsAuthenticate
9
+ include Socksproxyable::InstanceMethodsConnect
10
+
11
+ alias initialize_tcp initialize
12
+
13
+ attr_reader :socks_peer
14
+
15
+ # See http://tools.ietf.org/html/rfc1928
16
+ # rubocop:disable Metrics/ParameterLists
17
+ def initialize(host = nil, port = nil, local_host = nil, local_port = nil, **kwargs)
18
+ @socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress)
19
+ host = socks_peer.peer_host if socks_peer
20
+
21
+ if socks_server && socks_port && !socks_ignores.include?(host)
22
+ make_socks_connection(host, port, **kwargs)
23
+ else
24
+ make_direct_connection(host, port, local_host, local_port, **kwargs)
25
+ end
26
+ end
27
+ # rubocop:enable Metrics/ParameterLists
28
+
29
+ # string representation of the peer host address
30
+ class SOCKSConnectionPeerAddress < String
31
+ attr_reader :socks_server, :socks_port, :socks_username, :socks_password
32
+
33
+ def initialize(socks_server, socks_port, peer_host, socks_username = nil, socks_password = nil)
34
+ @socks_server = socks_server
35
+ @socks_port = socks_port
36
+ @socks_username = socks_username
37
+ @socks_password = socks_password
38
+ super(peer_host)
39
+ end
40
+
41
+ def inspect
42
+ "#{self} (via #{@socks_server}:#{@socks_port})"
43
+ end
44
+
45
+ def peer_host
46
+ to_s
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def socks_server
53
+ @socks_server ||= socks_peer ? socks_peer.socks_server : self.class.socks_server
54
+ end
55
+
56
+ def socks_port
57
+ @socks_port ||= socks_peer ? socks_peer.socks_port : self.class.socks_port
58
+ end
59
+
60
+ def socks_username
61
+ @socks_username ||= socks_peer ? socks_peer.socks_username : self.class.socks_username
62
+ end
63
+
64
+ def socks_password
65
+ @socks_password ||= socks_peer ? socks_peer.socks_password : self.class.socks_password
66
+ end
67
+
68
+ def socks_ignores
69
+ @socks_ignores ||= socks_peer ? [] : self.class.socks_ignores
70
+ end
71
+
72
+ def make_socks_connection(host, port, **kwargs)
73
+ Socksify.debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}"
74
+ initialize_tcp socks_server, socks_port, **kwargs
75
+ socks_authenticate(socks_username, socks_password) unless /^4/.match?(@socks_version)
76
+ socks_connect(host, port) if host
77
+ end
78
+
79
+ def make_direct_connection(host, port, local_host, local_port, **kwargs)
80
+ Socksify.debug_notice "Connecting directly to #{host}:#{port}"
81
+ initialize_tcp host, port, local_host, local_port, **kwargs
82
+ Socksify.debug_debug "Connected to #{host}:#{port}"
83
+ end
84
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # namespace
4
+ module Socksify
5
+ VERSION = '1.8.1'
6
+ end
data/lib/socksify.rb CHANGED
@@ -1,364 +1,135 @@
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
+ # frozen_string_literal: true
3
+
4
+ # Copyright (C) 2007 Stephan Maka <stephan@spaceboyz.net>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
18
 
19
19
  require 'socket'
20
20
  require 'resolv'
21
21
  require 'socksify/debug'
22
+ require 'socksify/tcpsocket'
22
23
 
24
+ # error class
23
25
  class SOCKSError < RuntimeError
24
26
  def initialize(msg)
25
- Socksify::debug_error("#{self.class}: #{msg}")
27
+ Socksify.debug_error("#{self.class}: #{msg}")
26
28
  super
27
29
  end
28
30
 
31
+ # rubocop:disable Style/Documentation
29
32
  class ServerFailure < SOCKSError
30
33
  def initialize
31
- super("general SOCKS server failure")
34
+ super('general SOCKS server failure')
32
35
  end
33
36
  end
37
+
34
38
  class NotAllowed < SOCKSError
35
39
  def initialize
36
- super("connection not allowed by ruleset")
40
+ super('connection not allowed by ruleset')
37
41
  end
38
42
  end
43
+
39
44
  class NetworkUnreachable < SOCKSError
40
45
  def initialize
41
- super("Network unreachable")
46
+ super('Network unreachable')
42
47
  end
43
48
  end
49
+
44
50
  class HostUnreachable < SOCKSError
45
51
  def initialize
46
- super("Host unreachable")
52
+ super('Host unreachable')
47
53
  end
48
54
  end
55
+
49
56
  class ConnectionRefused < SOCKSError
50
57
  def initialize
51
- super("Connection refused")
58
+ super('Connection refused')
52
59
  end
53
60
  end
61
+
54
62
  class TTLExpired < SOCKSError
55
63
  def initialize
56
- super("TTL expired")
64
+ super('TTL expired')
57
65
  end
58
66
  end
67
+
59
68
  class CommandNotSupported < SOCKSError
60
69
  def initialize
61
- super("Command not supported")
70
+ super('Command not supported')
62
71
  end
63
72
  end
73
+
64
74
  class AddressTypeNotSupported < SOCKSError
65
75
  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
76
+ super('Address type not supported')
140
77
  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
78
  end
79
+ # rubocop:enable Style/Documentation
264
80
 
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
81
+ RESPONSE_CODE_CLASSES = { 1 => ServerFailure,
82
+ 2 => NotAllowed,
83
+ 3 => NetworkUnreachable,
84
+ 4 => HostUnreachable,
85
+ 5 => ConnectionRefused,
86
+ 6 => TTLExpired,
87
+ 7 => CommandNotSupported,
88
+ 8 => AddressTypeNotSupported }.freeze
305
89
 
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
90
+ def self.for_response_code(code)
91
+ (resp = RESPONSE_CODE_CLASSES[code]) ? resp : self
318
92
  end
319
93
  end
320
94
 
95
+ # namespace
321
96
  module Socksify
322
97
  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
98
+ socket = TCPSocket.new # no args?
99
+ Socksify.debug_debug "Sending hostname to resolve: #{host}"
100
+ req = request(host)
101
+ socket.write req
102
+ addr, _port = socket.socks_receive_reply
103
+ Socksify.debug_notice "Resolved #{host} as #{addr} over SOCKS"
104
+ addr
105
+ ensure
106
+ socket.close
350
107
  end
351
108
 
352
109
  def self.proxy(server, port)
353
- default_server = TCPSocket::socks_server
354
- default_port = TCPSocket::socks_port
110
+ default_server = TCPSocket.socks_server
111
+ default_port = TCPSocket.socks_port
355
112
  begin
356
- TCPSocket::socks_server = server
357
- TCPSocket::socks_port = port
113
+ TCPSocket.socks_server = server
114
+ TCPSocket.socks_port = port
358
115
  yield
359
- ensure
360
- TCPSocket::socks_server = default_server
361
- TCPSocket::socks_port = default_port
116
+ ensure # failback
117
+ TCPSocket.socks_server = default_server
118
+ TCPSocket.socks_port = default_port
119
+ end
120
+ end
121
+
122
+ def self.request(host)
123
+ req = (+'') << "\005"
124
+ case host
125
+ when /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ # to IPv4 address
126
+ req << "\xF1\000\001#{(1..4).map { |i| Regexp.last_match(i).to_i }.pack('CCCC')}"
127
+ when /^[:0-9a-f]+$/ # to IPv6 address
128
+ raise 'TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor)'
129
+ # # req << "\004" # UNREACHABLE
130
+ else # to hostname
131
+ req << "\xF0\000\003#{[host.size].pack('C')}#{host}"
362
132
  end
133
+ req << [0].pack('n') # Port
363
134
  end
364
135
  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.1
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-20 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.7'
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: []