Skip to content

Commit ca69e45

Browse files
authored
Merge pull request #62 from anton-smagin/net-http-socksproxy-with-auth
Net http socksproxy with auth. Thanks for your contribution @anton-smagin and to everyone else who contributed towards this functionality, sorry it's taken so long to implement. I may change the last two parameters to keywords to avoid a long params list, but otherwise it looks looks great.
2 parents d486263 + 7975650 commit ca69e45

10 files changed

Lines changed: 131 additions & 66 deletions

File tree

.github/workflows/test.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ permissions:
1313
jobs:
1414
test:
1515
runs-on: ubuntu-latest
16+
17+
services:
18+
services:
19+
socks5:
20+
image: serjs/go-socks5-proxy
21+
env:
22+
PROXY_USER: user
23+
PROXY_PASSWORD: password
24+
ports:
25+
- 1080:1080
26+
1627
strategy:
1728
matrix:
1829
ruby: ['3.1', '3.2', '3.3', head]

ChangeLog

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,5 @@ SOCKSify Ruby 1.7.3
8181
===================
8282
* add Rakefile
8383
* fix missing :timeout kwarg in TCPSocket class (thanks @lizzypy)
84+
* Authentication support added to Net::HTTP.SOCKSProxy
85+
(thanks to @ojab and @anton-smagin)

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ Socksify.resolve("spaceboyz.net")
7070
```
7171
### Testing and Debugging
7272

73-
A tor proxy is required before running the tests. Install tor from your usual package manager, check it is running with `pidof tor` then run the tests with:
73+
A tor proxy and socks5 proxy with auth is required before running the tests.
74+
* Install tor from your usual package manager, check it is running with `pidof tor` then run the tests with:
75+
* Start a SOCKS5 proxy using Docker `docker run -d --name socks5 -p 1080:1080 -e PROXY_USER=user -e PROXY_PASSWORD=password serjs/go-socks5-proxy`
7476

7577
`bundle exec rake`
7678

@@ -102,4 +104,4 @@ Author
102104
License
103105
-------
104106
105-
SOCKSify Ruby is distributed under the terms of the GNU General Public License version 3 (see file `COPYING`) or the Ruby License (see file `LICENSE`) at your option.
107+
SOCKSify Ruby is distributed under the terms of the GNU General Public License version 3 (see file `COPYING`) or the Ruby License (see file `LICENSE`) at your option.

doc/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ <h3>Use Net::HTTP explicitly via SOCKS</h3>
105105
explicitly or use <code>Net::HTTP</code> directly.
106106
</p>
107107

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+
108116
<h3>Resolve addresses via SOCKS</h3>
109117
<pre>Socksify::resolve("spaceboyz.net")
110118
# => "87.106.131.203"</pre>

lib/socksify/http.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,44 @@
2121
module Net
2222
# patched class
2323
class HTTP
24-
def self.socks_proxy(p_host, p_port)
25-
proxyclass = Class.new(self)
26-
proxyclass.send(:include, SOCKSProxyDelta)
24+
def self.socks_proxy(p_host, p_port, p_username = nil, p_password = nil)
2725
proxyclass.module_eval do
2826
include Ruby3NetHTTPConnectable if RUBY_VERSION.to_f > 3.0 # patch #connect method
2927
include SOCKSProxyDelta::InstanceMethods
3028
extend SOCKSProxyDelta::ClassMethods
29+
3130
@socks_server = p_host
3231
@socks_port = p_port
32+
@socks_username = p_username
33+
@socks_password = p_password
3334
end
35+
3436
proxyclass
3537
end
3638

39+
def self.proxyclass
40+
@proxyclass ||= Class.new(self).tap { |klass| klass.send(:include, SOCKSProxyDelta) }
41+
end
42+
3743
class << self
3844
alias SOCKSProxy socks_proxy # legacy support for non snake case method name
3945
end
4046

4147
module SOCKSProxyDelta
4248
# class methods
4349
module ClassMethods
44-
attr_reader :socks_server, :socks_port
50+
attr_reader :socks_server, :socks_port,
51+
:socks_username, :socks_password
4552
end
4653

4754
# instance methods - no long supports Ruby < 2
4855
module InstanceMethods
4956
def address
50-
TCPSocket::SOCKSConnectionPeerAddress.new(self.class.socks_server, self.class.socks_port, @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+
)
5162
end
5263
end
5364
end

lib/socksify/socksproxyable.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def socks_version_hex
2626
# instance method #socks_authenticate
2727
module InstanceMethodsAuthenticate
2828
# rubocop:disable Metrics
29-
def socks_authenticate
30-
if self.class.socks_username || self.class.socks_password
29+
def socks_authenticate(socks_username, socks_password)
30+
if socks_username || socks_password
3131
Socksify.debug_debug 'Sending username/password authentication'
3232
write "\005\001\002"
3333
else
@@ -42,16 +42,16 @@ def socks_authenticate
4242
raise SOCKSError, "SOCKS version #{auth_reply[0..0]} not supported"
4343
end
4444

45-
if self.class.socks_username || self.class.socks_password
45+
if socks_username || socks_password
4646
if auth_reply[1..1] != "\002"
4747
raise SOCKSError, "SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported"
4848
end
4949

5050
auth = "\001"
51-
auth += self.class.socks_username.to_s.length.chr
52-
auth += self.class.socks_username.to_s
53-
auth += self.class.socks_password.to_s.length.chr
54-
auth += self.class.socks_password.to_s
51+
auth += socks_username.to_s.length.chr
52+
auth += socks_username.to_s
53+
auth += socks_password.to_s.length.chr
54+
auth += socks_password.to_s
5555
write auth
5656
auth_reply = recv(2)
5757
raise SOCKSError, 'SOCKS authentication failed' if auth_reply[1..1] != "\000"

lib/socksify/tcpsocket.rb

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ class TCPSocket
88

99
alias initialize_tcp initialize
1010

11+
attr_reader :socks_peer
12+
1113
# See http://tools.ietf.org/html/rfc1928
1214
# rubocop:disable Metrics/ParameterLists
1315
def initialize(host = nil, port = nil, local_host = nil, local_port = nil, **kwargs)
14-
socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress)
15-
socks_server = set_socks_server(socks_peer)
16-
socks_port = set_socks_port(socks_peer)
17-
socks_ignores = set_socks_ignores(socks_peer)
16+
@socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress)
1817
host = socks_peer.peer_host if socks_peer
18+
1919
if socks_server && socks_port && !socks_ignores.include?(host)
20-
make_socks_connection(host, port, socks_server, socks_port, **kwargs)
20+
make_socks_connection(host, port, **kwargs)
2121
else
2222
make_direct_connection(host, port, local_host, local_port, **kwargs)
2323
end
@@ -26,11 +26,13 @@ def initialize(host = nil, port = nil, local_host = nil, local_port = nil, **kwa
2626

2727
# string representation of the peer host address
2828
class SOCKSConnectionPeerAddress < String
29-
attr_reader :socks_server, :socks_port
29+
attr_reader :socks_server, :socks_port, :socks_username, :socks_password
3030

31-
def initialize(socks_server, socks_port, peer_host)
31+
def initialize(socks_server, socks_port, peer_host, socks_username = nil, socks_password = nil)
3232
@socks_server = socks_server
3333
@socks_port = socks_port
34+
@socks_username = socks_username
35+
@socks_password = socks_password
3436
super(peer_host)
3537
end
3638

@@ -45,22 +47,30 @@ def peer_host
4547

4648
private
4749

48-
def set_socks_server(socks_peer = nil)
49-
socks_peer ? socks_peer.socks_server : self.class.socks_server
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
5060
end
5161

52-
def set_socks_port(socks_peer = nil)
53-
socks_peer ? socks_peer.socks_port : self.class.socks_port
62+
def socks_password
63+
@socks_password ||= socks_peer ? socks_peer.socks_password : self.class.socks_password
5464
end
5565

56-
def set_socks_ignores(socks_peer = nil)
57-
socks_peer ? [] : self.class.socks_ignores
66+
def socks_ignores
67+
@socks_ignores ||= socks_peer ? [] : self.class.socks_ignores
5868
end
5969

60-
def make_socks_connection(host, port, socks_server, socks_port, **kwargs)
70+
def make_socks_connection(host, port, **kwargs)
6171
Socksify.debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}"
6272
initialize_tcp socks_server, socks_port, **kwargs
63-
socks_authenticate unless @socks_version =~ /^4/
73+
socks_authenticate(socks_username, socks_password) unless @socks_version =~ /^4/
6474
socks_connect(host, port) if host
6575
end
6676

test/test_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ def http_tor_proxy
2626
Net::HTTP.socks_proxy('127.0.0.1', 9050)
2727
end
2828

29+
def http_tor_proxy_with_auth(username, password)
30+
Net::HTTP.socks_proxy('127.0.0.1', 1080, username, password)
31+
end
32+
2933
def get_http(http_klass, url, host_header = nil)
3034
uri = URI(url)
3135
body = nil

test/test_socksify.rb

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require_relative 'test_helper'
4+
require_relative 'test_socksify_legacy'
45

56
# test class
67
class SocksifyTest < Minitest::Test
@@ -12,43 +13,7 @@ def self.test_order
1213
:alpha # until state between tests is fixed
1314
end
1415

15-
if RUBY_VERSION.to_f < 3.1 # test legacy methods TCPSocket.socks_server= and TCPSocket.socks_port=
16-
def test_check_tor
17-
disable_socks
18-
is_tor_direct, ip_direct = check_tor
19-
20-
refute is_tor_direct
21-
22-
enable_socks
23-
is_tor_socks, ip_socks = check_tor
24-
25-
assert is_tor_socks
26-
refute_equal ip_direct, ip_socks
27-
end
28-
29-
def test_check_tor_with_service_as_a_string
30-
disable_socks
31-
is_tor_direct, ip_direct = check_tor_with_service_as_string
32-
33-
refute is_tor_direct
34-
enable_socks
35-
is_tor_socks, ip_socks = check_tor_with_service_as_string
36-
37-
assert is_tor_socks
38-
39-
refute_equal ip_direct, ip_socks
40-
end
41-
42-
def test_connect_to_ip
43-
disable_socks
44-
ip_direct = internet_yandex_com_ip
45-
enable_socks
46-
ip_socks = internet_yandex_com_ip
47-
48-
refute_equal ip_direct, ip_socks
49-
end
50-
end
51-
# end legacy method tests
16+
include TestSocksifyLegacy
5217

5318
def test_check_tor_via_net_http
5419
disable_socks
@@ -69,6 +34,20 @@ def test_connect_to_ip_via_net_http
6934
refute_equal ip_direct, ip_socks
7035
end
7136

37+
def test_check_tor_via_net_http_with_auth
38+
disable_socks
39+
ip_address = internet_yandex_com_ip(http_tor_proxy_with_auth('user', 'password'))
40+
41+
assert_match(/\b\d{1,3}(\.\d{1,3}){3}\b/, ip_address)
42+
end
43+
44+
def test_check_tor_via_net_http_with_wrong_auth
45+
disable_socks
46+
assert_raises SOCKSError, 'SOCKS authentication failed' do
47+
internet_yandex_com_ip(http_tor_proxy_with_auth('user', 'bad_password'))
48+
end
49+
end
50+
7251
def test_ignores
7352
disable_socks
7453
tor_direct, ip_direct = check_tor

test/test_socksify_legacy.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module TestSocksifyLegacy
2+
if RUBY_VERSION.to_f < 3.1 # test legacy methods TCPSocket.socks_server= and TCPSocket.socks_port=
3+
def test_check_tor
4+
disable_socks
5+
is_tor_direct, ip_direct = check_tor
6+
7+
refute is_tor_direct
8+
9+
enable_socks
10+
is_tor_socks, ip_socks = check_tor
11+
12+
assert is_tor_socks
13+
refute_equal ip_direct, ip_socks
14+
end
15+
16+
def test_check_tor_with_service_as_a_string
17+
disable_socks
18+
is_tor_direct, ip_direct = check_tor_with_service_as_string
19+
20+
refute is_tor_direct
21+
enable_socks
22+
is_tor_socks, ip_socks = check_tor_with_service_as_string
23+
24+
assert is_tor_socks
25+
26+
refute_equal ip_direct, ip_socks
27+
end
28+
29+
def test_connect_to_ip
30+
disable_socks
31+
ip_direct = internet_yandex_com_ip
32+
enable_socks
33+
ip_socks = internet_yandex_com_ip
34+
35+
refute_equal ip_direct, ip_socks
36+
end
37+
end
38+
end

0 commit comments

Comments
 (0)