From 4af55424c9cd674391f93a02f4d95983bbf425f4 Mon Sep 17 00:00:00 2001 From: Musy Bite Date: Tue, 13 Mar 2018 22:42:05 +0300 Subject: [PATCH 1/3] Add authentication support to Net::HTTP.SOCKSProxy Inspired by #24. This version does not require thread-local variables, everything kept inside instance and local variables. Thanks @ojab! --- ChangeLog | 2 ++ doc/index.html | 8 ++++++++ lib/socksify/http.rb | 15 ++++++++++++--- lib/socksify/socksproxyable.rb | 14 +++++++------- lib/socksify/tcpsocket.rb | 30 ++++++++++++++++++++++++------ 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/ChangeLog b/ChangeLog index 27bc164..0ae2e87 100644 --- a/ChangeLog +++ b/ChangeLog @@ -81,3 +81,5 @@ SOCKSify Ruby 1.7.3 =================== * add Rakefile * fix missing :timeout kwarg in TCPSocket class (thanks @lizzypy) +* Authentication support added to Net::HTTP.SOCKSProxy + (thanks to @ojab) diff --git a/doc/index.html b/doc/index.html index 5d68607..86711d5 100644 --- a/doc/index.html +++ b/doc/index.html @@ -105,6 +105,14 @@

Use Net::HTTP explicitly via SOCKS

explicitly or use Net::HTTP directly.

+

+ Net::HTTP.SOCKSProxy also supports SOCKS authentication: +

+
+Net::HTTP.SOCKSProxy('127.0.0.1', 9050, 'username', 'p4ssw0rd')
+      
+ +

Resolve addresses via SOCKS

Socksify::resolve("spaceboyz.net")
 # => "87.106.131.203"
diff --git a/lib/socksify/http.rb b/lib/socksify/http.rb index 73eea48..a0629dd 100644 --- a/lib/socksify/http.rb +++ b/lib/socksify/http.rb @@ -21,16 +21,20 @@ module Net # patched class class HTTP - def self.socks_proxy(p_host, p_port) + def self.socks_proxy(p_host, p_port, p_username = nil, p_password = nil) proxyclass = Class.new(self) proxyclass.send(:include, SOCKSProxyDelta) proxyclass.module_eval do include Ruby3NetHTTPConnectable if RUBY_VERSION.to_f > 3.0 # patch #connect method include SOCKSProxyDelta::InstanceMethods extend SOCKSProxyDelta::ClassMethods + @socks_server = p_host @socks_port = p_port + @socks_username = p_username + @socks_password = p_password end + proxyclass end @@ -41,13 +45,18 @@ class << self module SOCKSProxyDelta # class methods module ClassMethods - attr_reader :socks_server, :socks_port + attr_reader :socks_server, :socks_port, + :socks_username, :socks_password end # instance methods - no long supports Ruby < 2 module InstanceMethods def address - TCPSocket::SOCKSConnectionPeerAddress.new(self.class.socks_server, self.class.socks_port, @address) + TCPSocket::SOCKSConnectionPeerAddress.new( + self.class.socks_server, self.class.socks_port, + @address, + self.class.socks_username, self.class.socks_password + ) end end end diff --git a/lib/socksify/socksproxyable.rb b/lib/socksify/socksproxyable.rb index d79f54f..4a5c26f 100644 --- a/lib/socksify/socksproxyable.rb +++ b/lib/socksify/socksproxyable.rb @@ -26,8 +26,8 @@ def socks_version_hex # instance method #socks_authenticate module InstanceMethodsAuthenticate # rubocop:disable Metrics - def socks_authenticate - if self.class.socks_username || self.class.socks_password + def socks_authenticate(socks_username, socks_password) + if socks_username || socks_password Socksify.debug_debug 'Sending username/password authentication' write "\005\001\002" else @@ -42,16 +42,16 @@ def socks_authenticate raise SOCKSError, "SOCKS version #{auth_reply[0..0]} not supported" end - if self.class.socks_username || self.class.socks_password + if socks_username || socks_password if auth_reply[1..1] != "\002" raise SOCKSError, "SOCKS authentication method #{auth_reply[1..1]} neither requested nor supported" end auth = "\001" - auth += self.class.socks_username.to_s.length.chr - auth += self.class.socks_username.to_s - auth += self.class.socks_password.to_s.length.chr - auth += self.class.socks_password.to_s + auth += username.to_s.length.chr + auth += socks_username.to_s + auth += socks_password.to_s.length.chr + auth += socks_password.to_s write auth auth_reply = recv(2) raise SOCKSError, 'SOCKS authentication failed' if auth_reply[1..1] != "\000" diff --git a/lib/socksify/tcpsocket.rb b/lib/socksify/tcpsocket.rb index 84ec288..0b7e1fe 100644 --- a/lib/socksify/tcpsocket.rb +++ b/lib/socksify/tcpsocket.rb @@ -10,14 +10,19 @@ class TCPSocket # See http://tools.ietf.org/html/rfc1928 # rubocop:disable Metrics/ParameterLists - def initialize(host = nil, port = nil, local_host = nil, local_port = nil, **kwargs) + def initialize(host = nil, port = nil, + local_host = nil, local_port = nil, + **kwargs) socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress) socks_server = set_socks_server(socks_peer) socks_port = set_socks_port(socks_peer) + socks_username = set_socks_username(socks_peer) + socks_password = set_socks_password(socks_peer) socks_ignores = set_socks_ignores(socks_peer) host = socks_peer.peer_host if socks_peer + if socks_server && socks_port && !socks_ignores.include?(host) - make_socks_connection(host, port, socks_server, socks_port, **kwargs) + make_socks_connection(host, port, socks_server, socks_port, socks_username, socks_password, **kwargs) else make_direct_connection(host, port, local_host, local_port, **kwargs) end @@ -26,11 +31,13 @@ def initialize(host = nil, port = nil, local_host = nil, local_port = nil, **kwa # string representation of the peer host address class SOCKSConnectionPeerAddress < String - attr_reader :socks_server, :socks_port + attr_reader :socks_server, :socks_port, :socks_username, :socks_password - def initialize(socks_server, socks_port, peer_host) + def initialize(socks_server, socks_port, peer_host, socks_username = nil, socks_password = nil) @socks_server = socks_server @socks_port = socks_port + @socks_username = socks_username + @socks_password = socks_password super(peer_host) end @@ -53,14 +60,25 @@ def set_socks_port(socks_peer = nil) socks_peer ? socks_peer.socks_port : self.class.socks_port end + def set_socks_username(socks_peer = nil) + socks_peer ? socks_peer.socks_username : self.class.socks_username + end + + def set_socks_password(socks_peer = nil) + socks_peer ? socks_peer.socks_password : self.class.socks_password + end + def set_socks_ignores(socks_peer = nil) socks_peer ? [] : self.class.socks_ignores end - def make_socks_connection(host, port, socks_server, socks_port, **kwargs) + def make_socks_connection(host, port, + socks_server, socks_port, + socks_username, socks_password, + **kwargs) Socksify.debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}" initialize_tcp socks_server, socks_port, **kwargs - socks_authenticate unless @socks_version =~ /^4/ + socks_authenticate(socks_username, socks_password) unless @socks_version =~ /^4/ socks_connect(host, port) if host end From 73bd8ceea0a039878446c66758dda0e7797d91f6 Mon Sep 17 00:00:00 2001 From: Anton Smagin Date: Fri, 11 Jul 2025 11:57:36 +0200 Subject: [PATCH 2/3] Refactor and fix rubocop errors --- lib/socksify/http.rb | 6 +++-- lib/socksify/socksproxyable.rb | 2 +- lib/socksify/tcpsocket.rb | 40 ++++++++++++++-------------------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/lib/socksify/http.rb b/lib/socksify/http.rb index a0629dd..46749ad 100644 --- a/lib/socksify/http.rb +++ b/lib/socksify/http.rb @@ -22,8 +22,6 @@ module Net # patched class class HTTP def self.socks_proxy(p_host, p_port, p_username = nil, p_password = nil) - proxyclass = Class.new(self) - proxyclass.send(:include, SOCKSProxyDelta) proxyclass.module_eval do include Ruby3NetHTTPConnectable if RUBY_VERSION.to_f > 3.0 # patch #connect method include SOCKSProxyDelta::InstanceMethods @@ -38,6 +36,10 @@ def self.socks_proxy(p_host, p_port, p_username = nil, p_password = nil) proxyclass end + def self.proxyclass + @proxyclass ||= Class.new(self).tap { |klass| klass.send(:include, SOCKSProxyDelta) } + end + class << self alias SOCKSProxy socks_proxy # legacy support for non snake case method name end diff --git a/lib/socksify/socksproxyable.rb b/lib/socksify/socksproxyable.rb index 4a5c26f..e97a096 100644 --- a/lib/socksify/socksproxyable.rb +++ b/lib/socksify/socksproxyable.rb @@ -48,7 +48,7 @@ def socks_authenticate(socks_username, socks_password) end auth = "\001" - auth += username.to_s.length.chr + auth += socks_username.to_s.length.chr auth += socks_username.to_s auth += socks_password.to_s.length.chr auth += socks_password.to_s diff --git a/lib/socksify/tcpsocket.rb b/lib/socksify/tcpsocket.rb index 0b7e1fe..f62ab99 100644 --- a/lib/socksify/tcpsocket.rb +++ b/lib/socksify/tcpsocket.rb @@ -8,21 +8,16 @@ class TCPSocket alias initialize_tcp initialize + attr_reader :socks_peer + # See http://tools.ietf.org/html/rfc1928 # rubocop:disable Metrics/ParameterLists - def initialize(host = nil, port = nil, - local_host = nil, local_port = nil, - **kwargs) - socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress) - socks_server = set_socks_server(socks_peer) - socks_port = set_socks_port(socks_peer) - socks_username = set_socks_username(socks_peer) - socks_password = set_socks_password(socks_peer) - socks_ignores = set_socks_ignores(socks_peer) + def initialize(host = nil, port = nil, local_host = nil, local_port = nil, **kwargs) + @socks_peer = host if host.is_a?(SOCKSConnectionPeerAddress) host = socks_peer.peer_host if socks_peer if socks_server && socks_port && !socks_ignores.include?(host) - make_socks_connection(host, port, socks_server, socks_port, socks_username, socks_password, **kwargs) + make_socks_connection(host, port, **kwargs) else make_direct_connection(host, port, local_host, local_port, **kwargs) end @@ -52,30 +47,27 @@ def peer_host private - def set_socks_server(socks_peer = nil) - socks_peer ? socks_peer.socks_server : self.class.socks_server + def socks_server + @socks_server ||= socks_peer ? socks_peer.socks_server : self.class.socks_server end - def set_socks_port(socks_peer = nil) - socks_peer ? socks_peer.socks_port : self.class.socks_port + def socks_port + @socks_port ||= socks_peer ? socks_peer.socks_port : self.class.socks_port end - def set_socks_username(socks_peer = nil) - socks_peer ? socks_peer.socks_username : self.class.socks_username + def socks_username + @socks_username ||= socks_peer ? socks_peer.socks_username : self.class.socks_username end - def set_socks_password(socks_peer = nil) - socks_peer ? socks_peer.socks_password : self.class.socks_password + def socks_password + @socks_password ||= socks_peer ? socks_peer.socks_password : self.class.socks_password end - def set_socks_ignores(socks_peer = nil) - socks_peer ? [] : self.class.socks_ignores + def socks_ignores + @socks_ignores ||= socks_peer ? [] : self.class.socks_ignores end - def make_socks_connection(host, port, - socks_server, socks_port, - socks_username, socks_password, - **kwargs) + def make_socks_connection(host, port, **kwargs) Socksify.debug_notice "Connecting to SOCKS server #{socks_server}:#{socks_port}" initialize_tcp socks_server, socks_port, **kwargs socks_authenticate(socks_username, socks_password) unless @socks_version =~ /^4/ From 7975650bdeeb145f0e1e22b32bede69768ca50a0 Mon Sep 17 00:00:00 2001 From: Anton Smagin Date: Fri, 11 Jul 2025 11:59:30 +0200 Subject: [PATCH 3/3] Add go-socks5-proxy dependency for CI and test with auth --- .github/workflows/test.yml | 11 ++++++++ ChangeLog | 2 +- README.md | 6 ++-- test/test_helper.rb | 4 +++ test/test_socksify.rb | 53 +++++++++++------------------------- test/test_socksify_legacy.rb | 38 ++++++++++++++++++++++++++ 6 files changed, 74 insertions(+), 40 deletions(-) create mode 100644 test/test_socksify_legacy.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8fa0b3..c213a24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,17 @@ permissions: jobs: test: runs-on: ubuntu-latest + + services: + services: + socks5: + image: serjs/go-socks5-proxy + env: + PROXY_USER: user + PROXY_PASSWORD: password + ports: + - 1080:1080 + strategy: matrix: ruby: ['3.1', '3.2', '3.3', head] diff --git a/ChangeLog b/ChangeLog index 0ae2e87..d5fef26 100644 --- a/ChangeLog +++ b/ChangeLog @@ -82,4 +82,4 @@ SOCKSify Ruby 1.7.3 * add Rakefile * fix missing :timeout kwarg in TCPSocket class (thanks @lizzypy) * Authentication support added to Net::HTTP.SOCKSProxy - (thanks to @ojab) + (thanks to @ojab and @anton-smagin) diff --git a/README.md b/README.md index 36f2f77..1181edc 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ Socksify.resolve("spaceboyz.net") ``` ### Testing and Debugging -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: +A tor proxy and socks5 proxy with auth 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: +* 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` `bundle exec rake` @@ -102,4 +104,4 @@ Author License ------- -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. \ No newline at end of file +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. diff --git a/test/test_helper.rb b/test/test_helper.rb index 2aa129c..7e99aab 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -26,6 +26,10 @@ def http_tor_proxy Net::HTTP.socks_proxy('127.0.0.1', 9050) end + def http_tor_proxy_with_auth(username, password) + Net::HTTP.socks_proxy('127.0.0.1', 1080, username, password) + end + def get_http(http_klass, url, host_header = nil) uri = URI(url) body = nil diff --git a/test/test_socksify.rb b/test/test_socksify.rb index 48b3e0d..048b8e4 100644 --- a/test/test_socksify.rb +++ b/test/test_socksify.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'test_helper' +require_relative 'test_socksify_legacy' # test class class SocksifyTest < Minitest::Test @@ -12,43 +13,7 @@ def self.test_order :alpha # until state between tests is fixed end - if RUBY_VERSION.to_f < 3.1 # test legacy methods TCPSocket.socks_server= and TCPSocket.socks_port= - def test_check_tor - disable_socks - is_tor_direct, ip_direct = check_tor - - refute is_tor_direct - - enable_socks - is_tor_socks, ip_socks = check_tor - - assert is_tor_socks - refute_equal ip_direct, ip_socks - end - - def test_check_tor_with_service_as_a_string - disable_socks - is_tor_direct, ip_direct = check_tor_with_service_as_string - - refute is_tor_direct - enable_socks - is_tor_socks, ip_socks = check_tor_with_service_as_string - - assert is_tor_socks - - refute_equal ip_direct, ip_socks - end - - def test_connect_to_ip - disable_socks - ip_direct = internet_yandex_com_ip - enable_socks - ip_socks = internet_yandex_com_ip - - refute_equal ip_direct, ip_socks - end - end - # end legacy method tests + include TestSocksifyLegacy def test_check_tor_via_net_http disable_socks @@ -69,6 +34,20 @@ def test_connect_to_ip_via_net_http refute_equal ip_direct, ip_socks end + def test_check_tor_via_net_http_with_auth + disable_socks + ip_address = internet_yandex_com_ip(http_tor_proxy_with_auth('user', 'password')) + + assert_match(/\b\d{1,3}(\.\d{1,3}){3}\b/, ip_address) + end + + def test_check_tor_via_net_http_with_wrong_auth + disable_socks + assert_raises SOCKSError, 'SOCKS authentication failed' do + internet_yandex_com_ip(http_tor_proxy_with_auth('user', 'bad_password')) + end + end + def test_ignores disable_socks tor_direct, ip_direct = check_tor diff --git a/test/test_socksify_legacy.rb b/test/test_socksify_legacy.rb new file mode 100644 index 0000000..3ba7541 --- /dev/null +++ b/test/test_socksify_legacy.rb @@ -0,0 +1,38 @@ +module TestSocksifyLegacy + if RUBY_VERSION.to_f < 3.1 # test legacy methods TCPSocket.socks_server= and TCPSocket.socks_port= + def test_check_tor + disable_socks + is_tor_direct, ip_direct = check_tor + + refute is_tor_direct + + enable_socks + is_tor_socks, ip_socks = check_tor + + assert is_tor_socks + refute_equal ip_direct, ip_socks + end + + def test_check_tor_with_service_as_a_string + disable_socks + is_tor_direct, ip_direct = check_tor_with_service_as_string + + refute is_tor_direct + enable_socks + is_tor_socks, ip_socks = check_tor_with_service_as_string + + assert is_tor_socks + + refute_equal ip_direct, ip_socks + end + + def test_connect_to_ip + disable_socks + ip_direct = internet_yandex_com_ip + enable_socks + ip_socks = internet_yandex_com_ip + + refute_equal ip_direct, ip_socks + end + end +end