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 @@
Net::HTTP directly.
+
+ Net::HTTP.SOCKSProxy also supports SOCKS authentication:
+
+Net::HTTP.SOCKSProxy('127.0.0.1', 9050, 'username', 'p4ssw0rd')
+
+
+
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