Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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)
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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.
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.
8 changes: 8 additions & 0 deletions doc/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ <h3>Use Net::HTTP explicitly via SOCKS</h3>
explicitly or use <code>Net::HTTP</code> directly.
</p>

<p>
<code>Net::HTTP.SOCKSProxy</code> also supports SOCKS authentication:
</p>
<pre>
Net::HTTP.SOCKSProxy('127.0.0.1', 9050, 'username', 'p4ssw0rd')
</pre>


<h3>Resolve addresses via SOCKS</h3>
<pre>Socksify::resolve("spaceboyz.net")
# => "87.106.131.203"</pre>
Expand Down
21 changes: 16 additions & 5 deletions lib/socksify/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,44 @@
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

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
Expand Down
14 changes: 7 additions & 7 deletions lib/socksify/socksproxyable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
40 changes: 25 additions & 15 deletions lib/socksify/tcpsocket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 16 additions & 37 deletions test/test_socksify.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require_relative 'test_helper'
require_relative 'test_socksify_legacy'

# test class
class SocksifyTest < Minitest::Test
Expand All @@ -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
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions test/test_socksify_legacy.rb
Original file line number Diff line number Diff line change
@@ -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