socks_handler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 38955165952ad878f7e2bef2bd818cc4b15232879c3ba967ab1ca62e1ad90bc1
4
+ data.tar.gz: 126326454aa18b570e72f02d3336c507c4ed63effbe3f95e09ac1eeb8ea019eb
5
+ SHA512:
6
+ metadata.gz: f1793dfddaede01289c23c9d319b1be7144526fdf65eae3ed0eca36a842cc44d4886447b4397b98c98816b9b556e7448b614f4b4ae07a6ec4a2a4149ca5ea695
7
+ data.tar.gz: fee6367cfd655a0266be4e71d323a0bfa6ffcf2794ab4fdc57afd2344cfe1fcac93226867d7a5b5c6afd86e1590f8bb23294311a79af7efda379f0c86b578c56
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in socks_handler.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ # For generating sig/defs.rbs
13
+ gem "yard"
14
+ gem "sord"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 abicky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # SocksHandler
2
+
3
+ SocksHandler is a flexible socksifier for sockets created by `TCPSocket.new`, `Socket.tcp`, or `UDPSocket.new` that solves the following issues:
4
+
5
+ * `SOCKSSocket` is not easy to use
6
+ - It is unavailable unless ruby is built with `--enable-socks`, and even if it is available, we cannot use domain names that the network where the program runs cannot resolve since socket classes, including `SOCKSSocket`, call `getaddrinfo` at initialization.
7
+ * Famous socksifiers such as [socksify](https://www.inet.no/dante/doc/1.3.x/socksify.1.html) and [proxychains4](https://github.com/rofl0r/proxychains-ng) don't support rules using domain names
8
+ - Besides, they don't work on macOS if Ruby is managed by rbenv maybe due to SIP (System Integrity Protection)
9
+
10
+ For more details, see the section "Related Work."
11
+
12
+ ## Installation
13
+
14
+ Install the gem and add to the application's Gemfile by executing:
15
+
16
+ $ bundle add socks_handler
17
+
18
+ If bundler is not being used to manage dependencies, install the gem by executing:
19
+
20
+ $ gem install socks_handler
21
+
22
+ ## Usage
23
+
24
+ ### Socksify TCP Connections
25
+
26
+ Assuming that a SOCKS server that can access the host "nginx" is listening on 127.0.0.1:1080. You can prepare such an environment with the following docker-compose.yml:
27
+
28
+ ```yaml
29
+ version: "2.4"
30
+ services:
31
+ sockd:
32
+ image: wernight/dante
33
+ ports:
34
+ - 1080:1080
35
+
36
+ nginx:
37
+ image: nginx
38
+ ```
39
+
40
+ Here is an example to create a socket that can access the host "nginx" from the Docker host:
41
+
42
+ ```ruby
43
+ require "socks_handler"
44
+
45
+ socket = TCPSocket.new("127.0.0.1", 1080) # or Socket.tcp("127.0.0.1", 1080)
46
+ SocksHandler::TCP.establish_connection(socket, "nginx", 80)
47
+
48
+ socket.write(<<~REQUEST.gsub("\n", "\r\n"))
49
+ HEAD / HTTP/1.1
50
+ Host: nginx
51
+
52
+ REQUEST
53
+ puts socket.gets #=> HTTP/1.1 200 OK
54
+ ```
55
+
56
+ If you want to access the host through the SOCKS server implicitly, you can use `SocksHandler.socksify` as follows:
57
+
58
+ ```ruby
59
+ require "socks_handler"
60
+
61
+ SocksHandler::TCP.socksify([
62
+ SocksHandler::ProxyAccessRule.new(
63
+ host_patterns: ["nginx"],
64
+ socks_server: "127.0.0.1:1080",
65
+ )
66
+ ])
67
+
68
+ socket = TCPSocket.new("nginx", 80)
69
+ socket.write(<<~REQUEST.gsub("\n", "\r\n"))
70
+ HEAD / HTTP/1.1
71
+ Host: nginx
72
+
73
+ REQUEST
74
+ puts socket.gets #=> HTTP/1.1 200 OK
75
+ ```
76
+
77
+ With `SocksHandler::TCP.socksify`, other methods using `TCPSocket.new` or `Socket.tcp` also access the remote host through the SOCKS server:
78
+
79
+ ```ruby
80
+ require "net/http"
81
+ require "socks_handler"
82
+
83
+ SocksHandler::TCP.socksify([
84
+ SocksHandler::ProxyAccessRule.new(
85
+ host_patterns: ["nginx"],
86
+ socks_server: "127.0.0.1:1080",
87
+ )
88
+ ])
89
+
90
+ Net::HTTP.start("nginx", 80) do |http|
91
+ pp http.head("/") #=> #<Net::HTTPOK 200 OK readbody=true>
92
+ end
93
+ ```
94
+
95
+ For more details, see the document of `SocksHandler::TCP.socksify`:
96
+
97
+ ```
98
+ $ ri SocksHandler::TCP.socksify
99
+ ```
100
+
101
+ ### Socksify UDP Connections
102
+
103
+ Assuming that a SOCKS server that can access the host "echo", which is a UDP echo server, is listening on 127.0.0.1:1080. You can prepare such an environment with the following docker-compose.yml:
104
+
105
+ ```yaml
106
+ version: "2.4"
107
+ services:
108
+ sockd:
109
+ image: wernight/dante
110
+ ports:
111
+ - 1080:1080
112
+ - 1024-1030:1024-1030/udp
113
+ sysctls:
114
+ net.ipv4.ip_local_port_range: "1024 1030"
115
+
116
+ echo:
117
+ image: abicky/ncat:latest
118
+ command: -e /bin/cat -kul 7
119
+ init: true
120
+ ```
121
+
122
+ Here is an example to create a socket that can access the host "nginx" from the Docker host:
123
+
124
+ ```ruby
125
+ require "socks_handler"
126
+
127
+ tcp_socket = TCPSocket.new("127.0.0.1", 1080) # or Socket.tcp("127.0.0.1", 1080)
128
+ udp_socket = SocksHandler::UDP.associate_udp(tcp_socket, "0.0.0.0", 0)
129
+
130
+ udp_socket.send("hello", 0, "echo", 7)
131
+ puts udp_socket.gets #=> hello
132
+ ```
133
+
134
+
135
+ ## Limitation
136
+
137
+ As `SocksHandler` only socksifies TCP connections created by `TCPSocket.new` or `Socket.tcp`, it doesn't socksify connections created by native extensions.
138
+
139
+ For example, assuming that a SOCKS server that can access the host "mysql" is listening on 127.0.0.1:1080 as follows:
140
+
141
+ ```yaml
142
+ version: "2.4"
143
+ services:
144
+ sockd:
145
+ image: wernight/dante
146
+
147
+ mysql:
148
+ image: mysql:8
149
+ environment:
150
+ MYSQL_ROOT_PASSWORD: password
151
+ ```
152
+
153
+ The following code raises `Mysql2::Error::ConnectionError` because the gem "mysql2" tries to connect to the server via a native extension:
154
+
155
+ ```ruby
156
+ require "mysql2"
157
+
158
+ SocksHandler.socksify([
159
+ SocksHandler::ProxyAccessRule.new(
160
+ host_patterns: ["mysql"],
161
+ socks_server: "127.0.0.1:1080",
162
+ )
163
+ ])
164
+
165
+ client = Mysql2::Client.new(
166
+ host: "mysql",
167
+ port: 3306,
168
+ username: "root",
169
+ password: "password",
170
+ )
171
+ client.ping
172
+ #=> Unknown MySQL server host 'mysql' (8) (Mysql2::Error::ConnectionError)
173
+ ```
174
+
175
+ ## Related Work
176
+
177
+ ### Gems
178
+
179
+ The following projects provide similar gems:
180
+
181
+ * [ruby-proxifier](https://github.com/samuelkadolph/ruby-proxifier)
182
+ - This project seems to no longer be maintained.
183
+ * [socksify-ruby](https://github.com/astro/socksify-ruby)
184
+
185
+ ### SOCKSSocket
186
+
187
+ On macOS, you can build ruby with `SOCKSSocket` as follows:
188
+
189
+ ```
190
+ $ brew install dante bison
191
+ $ git clone https://github.com/ruby/ruby.git
192
+ $ cd ruby
193
+ $ git checkout v3_2_2
194
+ $ ./configure --enable-socks
195
+ $ PATH="/usr/local/opt/bison/bin:$PATH" make -j$(nproc) install
196
+ $ cat <<EOF >/etc/socks.conf
197
+ route {
198
+ from: 0.0.0.0/0 to: 0.0.0.0/0 via: 127.0.0.1 port = 1080
199
+ proxyprotocol: socks_v5
200
+ method: none
201
+ }
202
+ EOF
203
+ ```
204
+
205
+ Here is example code to access nginx launched using docker-compose.yml in the section "Usage":
206
+
207
+ ```ruby
208
+ require "socket"
209
+
210
+ ip = `docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker compose ps -q nginx)`.chomp
211
+ [ip, "nginx"].each do |host|
212
+ puts "Send an HTTP request to #{host}"
213
+ socket = SOCKSSocket.new(host, 80)
214
+ socket.write(<<~REQUEST.gsub("\n", "\r\n"))
215
+ HEAD / HTTP/1.1
216
+ Host: nginx
217
+
218
+ REQUEST
219
+ puts "Received: #{socket.gets}"
220
+ end
221
+ ```
222
+
223
+ As you can see below, we cannot use the domain name "nginx" since socket classes, including `SOCKSSocket`, call `getaddrinfo` at initialization:
224
+
225
+ ```
226
+ $ /usr/local/bin/ruby /path/to/code.rb
227
+ Send an HTTP request to 192.168.160.4
228
+ Received: HTTP/1.1 200 OK
229
+ Send an HTTP request to nginx
230
+ code.rb:6:in `initialize': getaddrinfo: nodename nor servname provided, or not known (SocketError)
231
+ from code.rb:6:in `new'
232
+ from code.rb:6:in `block in <main>'
233
+ from code.rb:4:in `each'
234
+ from code.rb:4:in `<main>'
235
+ ```
236
+
237
+ ### ProxyChains-NG
238
+
239
+ [ProxyChains-NG](https://github.com/rofl0r/proxychains-ng) is a socksifier that works well even on macOS.
240
+
241
+ On macOS, you can install it as follows:
242
+
243
+ ```
244
+ $ brew install proxychains-ng
245
+ ```
246
+
247
+ Then edit `/usr/local/etc/proxychains.conf` to use the socks server listening on 127.0.0.1:1080:
248
+
249
+ ```
250
+ [ProxyList]
251
+ socks5 127.0.0.1 1080
252
+ ```
253
+
254
+ ProxyChains-NG can socksify even connections created by native extensions. Here is example code to demonstrate it:
255
+
256
+ ```ruby
257
+ require "net/http"
258
+ require "mysql2"
259
+
260
+ Net::HTTP.start("nginx", 80) do |http|
261
+ pp http.head("/")
262
+ end
263
+
264
+ client = Mysql2::Client.new(
265
+ host: "mysql",
266
+ port: 3306,
267
+ username: "root",
268
+ password: "password",
269
+ )
270
+ puts client.ping
271
+ ```
272
+
273
+ As you can see below, the program can access containers though the socks server:
274
+
275
+ ```
276
+ $ proxychains4 /usr/local/bin/ruby /path/to/code.rb
277
+ [proxychains] config file found: /usr/local/etc/proxychains.conf
278
+ [proxychains] preloading /usr/local/Cellar/proxychains-ng/4.16/lib/libproxychains4.dylib
279
+ [proxychains] DLL init: proxychains-ng 4.16
280
+ [proxychains] Strict chain ... 127.0.0.1:1080 ... nginx:80 ... OK
281
+ #<Net::HTTPOK 200 OK readbody=true>
282
+ [proxychains] Strict chain ... 127.0.0.1:1080 ... mysql:3306 ... OK
283
+ true
284
+ ```
285
+
286
+ However, it doesn't work if Ruby is managed by rbenv:
287
+
288
+ ```
289
+ $ rbenv local
290
+ 3.2.2
291
+ $ proxychains4 ruby /path/to/code.rb
292
+ [proxychains] config file found: /usr/local/etc/proxychains.conf
293
+ [proxychains] preloading /usr/local/Cellar/proxychains-ng/4.16/lib/libproxychains4.dylib
294
+ /Users/arabiki/.anyenv/envs/rbenv/versions/3.2.2/lib/ruby/3.2.0/net/http.rb:1271:in `initialize': Failed to open TCP connection to nginx:80 (getaddrinfo: nodename nor servname provided, or not known) (SocketError)
295
+ -- snip --
296
+ ```
297
+
298
+ Maybe the reason is that macOS doesn't allow any system binaries to preload libraries and rbenv uses `/usr/bin/env`:
299
+
300
+ ```
301
+ $ proxychains4 env /usr/local/bin/ruby /path/to/code.rb
302
+ [proxychains] config file found: /usr/local/etc/proxychains.conf
303
+ [proxychains] preloading /usr/local/Cellar/proxychains-ng/4.16/lib/libproxychains4.dylib
304
+ /usr/local/lib/ruby/3.2.0/net/http.rb:1271:in `initialize': Failed to open TCP connection to nginx:80 (getaddrinfo: nodename nor servname provided, or not known) (SocketError)
305
+ -- snip --
306
+ ```
307
+
308
+ Although ProxyChains-NG works well in almost all cases, it cannot use domain names to determine whether to access them through a socks proxy.
309
+
310
+
311
+ ## Development
312
+
313
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
314
+
315
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
316
+
317
+ ## Contributing
318
+
319
+ Bug reports and pull requests are welcome on GitHub at https://github.com/abicky/socks_handler.
320
+
321
+ ## License
322
+
323
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ task :generate_rbs do
11
+ sh "sord sig/defs.rbs"
12
+ sh %Q{sed -i#{" " if RUBY_PLATFORM.include?("darwin")}'' '1s/^/# This file was generated by "rake generate_rbs"\\n\\n/' sig/defs.rbs}
13
+ # Workaround for the error "Detected recursive ancestors: ::SocksHandler::UDPSocket < ::SocksHandler::UDPSocket"
14
+ sh %Q{sed -i#{" " if RUBY_PLATFORM.include?("darwin")}'' 's/UDPSocket < UDPSocket/UDPSocket < ::UDPSocket/' sig/defs.rbs}
15
+ end
@@ -0,0 +1,89 @@
1
+ version: "2.4"
2
+ services:
3
+ create-work-directory:
4
+ image: wernight/dante
5
+ entrypoint: ""
6
+ command: |
7
+ sh -c '
8
+ cat <<EOF >/work/entrypoint.sh
9
+ #!/bin/sh
10
+ set -e
11
+
12
+ # Allow IPv6 access
13
+ sed -i "s|from: 0.0.0.0/0 to: 0.0.0.0/0|from: 0/0 to: 0/0|" /etc/sockd.conf
14
+
15
+ if [ -n "\$$USERNAME" ]; then
16
+ sed -i "s/^ \#socksmethod: username/ socksmethod: username/" /etc/sockd.conf
17
+
18
+ printf "\$$PASSWORD\\n\$$PASSWORD\\n" | adduser \$$USERNAME
19
+ fi
20
+ exec sockd
21
+
22
+ EOF
23
+
24
+ chmod +x /work/entrypoint.sh
25
+ '
26
+ volumes:
27
+ - type: volume
28
+ source: workdir
29
+ target: /work
30
+
31
+ sockd-auth-none:
32
+ image: wernight/dante
33
+ command: /work/entrypoint.sh
34
+ ports:
35
+ - 1080:1080
36
+ - 1024-1030:1024-1030/udp
37
+ volumes:
38
+ - type: volume
39
+ source: workdir
40
+ target: /work
41
+ sysctls:
42
+ net.ipv4.ip_local_port_range: "1024 1030"
43
+ depends_on:
44
+ create-work-directory:
45
+ condition: service_completed_successfully
46
+
47
+ sockd-auth-username-password:
48
+ image: wernight/dante
49
+ command: /work/entrypoint.sh
50
+ environment:
51
+ USERNAME: user
52
+ PASSWORD: pass
53
+ ports:
54
+ - 1081:1080
55
+ volumes:
56
+ - type: volume
57
+ source: workdir
58
+ target: /work
59
+ depends_on:
60
+ create-work-directory:
61
+ condition: service_completed_successfully
62
+
63
+ nginx:
64
+ image: nginx
65
+
66
+ echo:
67
+ image: abicky/ncat:latest
68
+ command: -e /bin/cat -kul 7
69
+ init: true
70
+
71
+ ruby:
72
+ image: ruby:3.2.2
73
+ command: "sleep 86400"
74
+ volumes:
75
+ - type: bind
76
+ source: "."
77
+ target: "/work"
78
+ working_dir: /work
79
+ init: true
80
+ volumes:
81
+ workdir:
82
+
83
+ networks:
84
+ default:
85
+ enable_ipv6: true
86
+ ipam:
87
+ config:
88
+ - subnet: 2001:3984:3989::/64
89
+ gateway: 2001:3984:3989::1
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ module AddressType
5
+ # @return [Integer]
6
+ IPV4 = 0x01
7
+ # @return [Integer]
8
+ DOMAINNAME = 0x03
9
+ # @return [Integer]
10
+ IPV6 = 0x04
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ module AuthenticationMethod
5
+ # @return [Integer]
6
+ NONE = 0x00
7
+ # @return [Integer]
8
+ GSSAPI = 0x01 # Not supported yet (https://www.ietf.org/rfc/rfc1961.txt)
9
+ # @return [Integer]
10
+ USERNAME_PASSWORD = 0x02
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ module Command
5
+ # @return [Integer]
6
+ CONNECT = 0x01
7
+ # @return [Integer]
8
+ BIND = 0x02 # Not supported yet
9
+ # @return [Integer]
10
+ UDP_ASSOCIATE = 0x03
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require "socks_handler/rule"
3
+
4
+ module SocksHandler
5
+ class DirectAccessRule < Rule
6
+ # @param host_patterns [Array<String, Regexp>]
7
+ def initialize(host_patterns:)
8
+ super(host_patterns: host_patterns)
9
+ end
10
+
11
+ # @return [true]
12
+ def direct
13
+ true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ class UnsupportedProtocol < StandardError; end
5
+
6
+ class NoAcceptableMethods < StandardError; end
7
+
8
+ class AuthenticationFailure < StandardError; end
9
+
10
+ class RelayRequestFailure < StandardError
11
+ # @param code [Integer]
12
+ def initialize(code)
13
+ case code
14
+ when 0x01
15
+ super("general SOCKS server failure")
16
+ when 0x02
17
+ super("connection not allowed by ruleset")
18
+ when 0x03
19
+ super("Network unreachable")
20
+ when 0x04
21
+ super("Host unreachable")
22
+ when 0x05
23
+ super("Connection refused")
24
+ when 0x06
25
+ super("TTL expired")
26
+ when 0x07
27
+ super("Command not supported")
28
+ when 0x08
29
+ super("Address type not supported")
30
+ else
31
+ super("Unknown error")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require "socks_handler/rule"
3
+
4
+ module SocksHandler
5
+ class ProxyAccessRule < Rule
6
+ # @param host_patterns [Array<String, Regexp>]
7
+ # @param socks_server [String] a string in the format "<host>:<port>"
8
+ # @param username [String, nil]
9
+ # @param password [String, nil]
10
+ def initialize(host_patterns:, socks_server:, username: nil, password: nil)
11
+ host, port = socks_server.split(":")
12
+ super(host_patterns: host_patterns, host: host, port: port.to_i, username: username, password: password)
13
+ end
14
+
15
+ # @return [false]
16
+ def direct
17
+ false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ # @!attribute [r] host
5
+ # @return [String, nil]
6
+ # @!attribute [r] port
7
+ # @return [Integer, nil]
8
+ # @!attribute [r] username
9
+ # @return [String, nil]
10
+ # @!attribute [r] password
11
+ # @return [String, nil]
12
+ # @!attribute [r] host_patterns
13
+ # @return [Array<String, Regexp>]
14
+ class Rule
15
+ attr_reader :host, :port, :username, :password, :host_patterns
16
+
17
+ # @return [Rule]
18
+ def self.new(**kwargs)
19
+ raise "Rule is an abstract class" if self == Rule
20
+ super
21
+ end
22
+
23
+ # @param host_patterns [Array<String, Regexp>]
24
+ # @param host [String, nil]
25
+ # @param port [Integer, nil]
26
+ # @param username [String, nil]
27
+ # @param password [String, nil]
28
+ def initialize(host_patterns:, host: nil, port: nil, username: nil, password: nil)
29
+ @host = host
30
+ @port = port
31
+ @username = username
32
+ @password = password
33
+ self.host_patterns = host_patterns
34
+ end
35
+
36
+ # @return [Boolean]
37
+ def direct
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # @param value [Array<String, Regexp>]
42
+ # @return [Array<String, Regexp>]
43
+ def host_patterns=(value)
44
+ raise ArgumentError, "host_patterns is not an array" unless value.is_a?(Array)
45
+ @host_patterns = value.frozen? ? value : value.dup.freeze
46
+ @remote_host_pattern_regex = Regexp.union(convert_regexps(value))
47
+ value
48
+ end
49
+
50
+ # @param remote_host [String]
51
+ # @return [Boolean]
52
+ def match?(remote_host)
53
+ @remote_host_pattern_regex.match?(remote_host)
54
+ end
55
+
56
+ private
57
+
58
+ # @param value [Array<String, Regexp>]
59
+ # @return [Array<Regexp>]
60
+ def convert_regexps(value)
61
+ value.map do |v|
62
+ v.is_a?(Regexp) ? v : Regexp.new("\\A#{Regexp.escape(v)}\\z")
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SocksHandler
4
+ module SocketSocksify
5
+ # @param remote_host [String]
6
+ # @param remote_port [Integer, String]
7
+ # @param local_host [String, nil]
8
+ # @param local_port [Integer, String]
9
+ # @param connect_timeout [Integer, Float, nil]
10
+ # @param resolv_timeout [Integer, Float, nil]
11
+ # @return [Socket]
12
+ def tcp(remote_host, remote_port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, &block)
13
+ rule = SocksHandler::TCP.find_rule(remote_host)
14
+ return super if rule.nil? || rule.direct
15
+
16
+ socket = super(rule.host, rule.port, local_host, local_port, connect_timeout: connect_timeout, resolv_timeout: resolv_timeout, &block)
17
+ begin
18
+ SocksHandler::TCP.establish_connection(socket, remote_host, remote_port, rule.username, rule.password)
19
+ rescue
20
+ socket.close
21
+ raise
22
+ end
23
+
24
+ socket
25
+ end
26
+ end
27
+ end