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 27bc164..d5fef26 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 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/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..46749ad 100644 --- a/lib/socksify/http.rb +++ b/lib/socksify/http.rb @@ -21,19 +21,25 @@ module Net # patched class class HTTP - def self.socks_proxy(p_host, p_port) - proxyclass = Class.new(self) - proxyclass.send(:include, SOCKSProxyDelta) + def self.socks_proxy(p_host, p_port, p_username = nil, p_password = nil) 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 + 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 @@ -41,13 +47,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..e97a096 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 += socks_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..f62ab99 100644 --- a/lib/socksify/tcpsocket.rb +++ b/lib/socksify/tcpsocket.rb @@ -8,16 +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_ignores = set_socks_ignores(socks_peer) + @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, **kwargs) + make_socks_connection(host, port, **kwargs) else make_direct_connection(host, port, local_host, local_port, **kwargs) end @@ -26,11 +26,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 @@ -45,22 +47,30 @@ 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 socks_port + @socks_port ||= socks_peer ? socks_peer.socks_port : self.class.socks_port + end + + def socks_username + @socks_username ||= socks_peer ? socks_peer.socks_username : self.class.socks_username end - def set_socks_port(socks_peer = nil) - socks_peer ? socks_peer.socks_port : self.class.socks_port + 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, **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 unless @socks_version =~ /^4/ + socks_authenticate(socks_username, socks_password) unless @socks_version =~ /^4/ socks_connect(host, port) if host end 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