├── .gitignore
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── bin
├── pirb
└── pruby
├── lib
├── proxifier.rb
├── proxifier
│ ├── env.rb
│ ├── proxies
│ │ ├── http.rb
│ │ ├── socks.rb
│ │ ├── socks4.rb
│ │ └── socks4a.rb
│ ├── proxy.rb
│ └── version.rb
└── uri
│ └── socks.rb
└── proxifier.gemspec
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2011 by Samuel Kadolph
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ruby-proxifier
2 |
3 | ## Installing
4 |
5 | ### Recommended
6 |
7 | ```
8 | gem install proxifier
9 | ```
10 |
11 | ### Edge
12 |
13 | ```
14 | git clone https://github.com/samuelkadolph/ruby-proxifier
15 | cd ruby-proxifier && rake install
16 | ```
17 |
18 | ## Rationale
19 |
20 | This gem was created for 2 purposes.
21 |
22 | First is to enable ruby programmers to use HTTP or SOCKS proxies
23 | interchangeably when using TCPSockets. Either manually with
24 | `Proxifier::Proxy#open` or by `require "proxifier/env"`.
25 |
26 | The second purpose is to use ruby code that doesn't use proxies for users that
27 | have to use proxies.
The pruby and pirb executables are simple wrappers for
28 | their respective ruby executables that support proxies from environment
29 | variables.
30 |
31 | ## Usage
32 |
33 | ### Executable Wrappers & Environment Variables
34 |
35 | proxifier provides two executables: `pruby` and `pirb`. They are simple
36 | wrappers for your current `ruby` and `irb` executables that requires the
37 | `"proxifier/env"` script which installs hooks into `TCPSocket` which will use
38 | the proxy environment variables to proxy any `TCPSocket`.
39 |
40 | The environment variables that proxifier will check are (in order of descending
41 | precedence):
42 |
43 |
44 |
45 | Variable Name |
46 | Alternatives |
47 | Notes |
48 |
49 |
50 | proxy |
51 | PROXY |
52 | Requires the proxy scheme to be present. |
53 |
54 |
55 | socks_proxy |
56 | SOCKS_PROXY socks5_proxy SOCKS5_PROXY |
57 | Implies the SOCKS5 proxy scheme. |
58 |
59 |
60 | socks4a_proxy |
61 | SOCKS4A_PROXY |
62 | Implies the SOCKS4A proxy scheme. |
63 |
64 |
65 | socks4_proxy |
66 | PROXY |
67 | Implies the SOCKS4 proxy scheme. |
68 |
69 |
70 | http_proxy |
71 | HTTP_PROXY |
72 | Implies the HTTP proxy scheme. |
73 |
74 |
75 |
76 | ### Ruby
77 |
78 | ```ruby
79 | require "proxifier/proxy"
80 |
81 | proxy = Proxifier::Proxy("socks://localhost")
82 | socket = proxy.open("www.google.com", 80)
83 | socket << "GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n"
84 | socket.gets # => "HTTP/1.1 200 OK\r\n"
85 | ```
86 |
87 | ## Supported Proxies
88 |
89 |
90 |
91 | Protocol |
92 | Formats |
93 | Notes |
94 |
95 |
96 | HTTP |
97 | http://[username[:password]@]host[:port][?tunnel=false] |
98 |
99 | The port defaults to 80. This is currently a limitation that may be solved in the future.
100 | Appending ?tunnel=false forces the proxy to not use CONNECT . |
101 |
102 |
103 | SOCKS5 |
104 | socks://[username[:password]@]host[:port]
105 | socks5://[username[:password]@]host[:port] |
106 |
107 | Port defaults to 1080.
108 | |
109 |
110 |
111 | SOCKS4A |
112 | socks4a://[username@]host[:port] |
113 | Not yet implemented. |
114 |
115 |
116 | SOCKS4 |
117 | socks4://[username@]host[:port] |
118 | Currently hangs. Not sure if the problem is with code or server. |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
--------------------------------------------------------------------------------
/bin/pirb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | executable = File.expand_path("../" + Gem.default_exec_format % "irb", Gem.ruby)
4 | full_gem_path = Gem.loaded_specs["proxifier"].full_gem_path
5 | load_paths = Gem.loaded_specs["proxifier"].require_paths.map { |p| "-I#{File.join(full_gem_path, p)}" }
6 | # TODO: support argument switches
7 |
8 | exec(executable, *load_paths, "-rproxifier/env", *ARGV)
9 |
--------------------------------------------------------------------------------
/bin/pruby:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | executable = Gem.ruby
4 | full_gem_path = Gem.loaded_specs["proxifier"].full_gem_path
5 | load_paths = Gem.loaded_specs["proxifier"].require_paths.map { |p| "-I#{File.join(full_gem_path, p)}" }
6 | # TODO: support argument switches
7 |
8 | exec(executable, *load_paths, "-rproxifier/env", *ARGV)
9 |
--------------------------------------------------------------------------------
/lib/proxifier.rb:
--------------------------------------------------------------------------------
1 | require "uri"
2 | require "uri/socks"
3 |
4 | module Proxifier
5 | require "proxifier/version"
6 |
7 | autoload :HTTPProxy, "proxifier/proxies/http"
8 | autoload :SOCKSProxy, "proxifier/proxies/socks"
9 | autoload :SOCKS5Proxy, "proxifier/proxies/socks"
10 | autoload :SOCKS4Proxy, "proxifier/proxies/socks4"
11 | autoload :SOCKS4AProxy, "proxifier/proxies/socks4a"
12 |
13 | def self.Proxy(url, options = {})
14 | url = URI.parse(url)
15 |
16 | raise(ArgumentError, "proxy url has no scheme") unless url.scheme
17 | begin
18 | klass = const_get("#{url.scheme.upcase}Proxy")
19 | rescue NameError
20 | raise(ArgumentError, "unknown proxy scheme `#{url.scheme}'")
21 | end
22 |
23 | klass.new(url, options)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/proxifier/env.rb:
--------------------------------------------------------------------------------
1 | require "socket"
2 | require "proxifier"
3 |
4 | module Proxifier
5 | class Proxy
6 | def open(host, port, local_host = nil, local_port = nil)
7 | return TCPSocket.new(host, port, local_host, local_port, :proxy => nil) unless proxify?(host)
8 |
9 | socket = TCPSocket.new(self.host, self.port, local_host, local_port, :proxy => nil)
10 |
11 | begin
12 | proxify(socket, host, port)
13 | rescue
14 | socket.close
15 | raise
16 | end
17 |
18 | socket
19 | end
20 | end
21 |
22 | module Proxify
23 | def self.included(klass)
24 | klass.class_eval do
25 | alias_method :initialize_without_proxy, :initialize
26 | alias_method :initialize, :initialize_with_proxy
27 | end
28 | end
29 |
30 | def initialize_with_proxy(host, port, options_or_local_host = {}, local_port = nil, options_if_local_host = {})
31 | if options_or_local_host.is_a?(Hash)
32 | local_host = nil
33 | options = options_or_local_host
34 | else
35 | local_host = options_or_local_host
36 | options = options_if_local_host
37 | end
38 |
39 | if options[:proxy] && (proxy = Proxifier::Proxy(options.delete(:proxy), options)) && proxy.proxify?(host)
40 | initialize_without_proxy(proxy.host, proxy.port, local_host, local_port)
41 | begin
42 | proxy.proxify(self, host, port)
43 | rescue
44 | close
45 | raise
46 | end
47 | else
48 | initialize_without_proxy(host, port, local_host, local_port)
49 | end
50 | end
51 | end
52 |
53 | module EnvironmentProxify
54 | def self.included(klass)
55 | klass.class_eval do
56 | extend ClassMethods
57 | alias_method :initialize_without_environment_proxy, :initialize
58 | alias_method :initialize, :initialize_with_environment_proxy
59 | end
60 | end
61 |
62 | def initialize_with_environment_proxy(host, port, options_or_local_host = {}, local_port = nil, options_if_local_host = {})
63 | if options_or_local_host.is_a?(Hash)
64 | local_host = nil
65 | options = options_or_local_host
66 | else
67 | local_host = options_or_local_host
68 | options = options_if_local_host
69 | end
70 |
71 | options = { :proxy => environment_proxy, :no_proxy => environment_no_proxy }.merge(options)
72 | initialize_without_environment_proxy(host, port, local_host, local_port, options)
73 | end
74 |
75 | def environment_proxy
76 | self.class.environment_proxy
77 | end
78 |
79 | def environment_no_proxy
80 | self.class.environment_no_proxy
81 | end
82 |
83 | module ClassMethods
84 | def environment_proxy
85 | ENV["proxy"] || ENV["PROXY"] || specific_environment_proxy
86 | end
87 |
88 | def environment_no_proxy
89 | ENV["no_proxy"] || ENV["NO_PROXY"]
90 | end
91 |
92 | private
93 | def specific_environment_proxy
94 | %w(socks socks5 socks4a socks4 http).each do |type|
95 | if proxy = ENV["#{type}_proxy"] || ENV["#{type.upcase}_PROXY"]
96 | scheme = "#{type}://"
97 |
98 | proxy = proxy.dup
99 | proxy.insert(0, scheme) unless proxy.index(scheme) == 0
100 | return proxy
101 | end
102 | end
103 |
104 | nil
105 | end
106 | end
107 | end
108 | end
109 |
110 | class TCPSocket
111 | include Proxifier::Proxify
112 | include Proxifier::EnvironmentProxify
113 | end
114 |
--------------------------------------------------------------------------------
/lib/proxifier/proxies/http.rb:
--------------------------------------------------------------------------------
1 | require "net/http"
2 | require "proxifier/proxy"
3 |
4 | module Proxifier
5 | class HTTPProxy < Proxy
6 | def do_proxify(socket, host, port)
7 | return if query_options["tunnel"] == "false"
8 |
9 | socket << "CONNECT #{host}:#{port} HTTP/1.1\r\n"
10 | socket << "Host: #{host}:#{port}\r\n"
11 | socket << "Proxy-Authorization: Basic #{["#{user}:#{password}"].pack("m").chomp}\r\n" if user
12 | socket << "\r\n"
13 |
14 | buffer = Net::BufferedIO.new(socket)
15 | response = Net::HTTPResponse.read_new(buffer)
16 | response.error! unless response.is_a?(Net::HTTPOK)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/proxifier/proxies/socks.rb:
--------------------------------------------------------------------------------
1 | require "ipaddr"
2 | require "proxifier/proxy"
3 |
4 | module Proxifier
5 | class SOCKSProxy < Proxy
6 | VERSION = 0x05
7 |
8 | def do_proxify(socket, host, port)
9 | authenticaton_method = greet(socket)
10 | authenticate(socket, authenticaton_method)
11 | connect(socket, host, port)
12 | end
13 |
14 | protected
15 | def greet(socket)
16 | methods = authentication_methods
17 |
18 | socket << [VERSION, methods.size, *methods].pack("CCC#{methods.size}")
19 | version, authentication_method = socket.read(2).unpack("CC")
20 | check_version(version)
21 |
22 | authentication_method
23 | end
24 |
25 | def authenticate(socket, method)
26 | case method
27 | when 0x00 # NO AUTHENTICATION REQUIRED
28 | when 0x02 # USERNAME/PASSWORD
29 | user &&= user[0, 0xFF]
30 | password &&= password[0, 0xFF]
31 |
32 | socket << [user.size, user, password.size, password].pack("CA#{user.size}CA#{password.size}")
33 | version, status = socket.read(2).unpack("CC")
34 | check_version(version)
35 |
36 | case status
37 | when 0x00 # SUCCESS
38 | else
39 | raise "SOCKS5 username/password authentication failed"
40 | end
41 | else
42 | raise "no acceptable SOCKS5 authentication methods"
43 | end
44 | end
45 |
46 | def connect(socket, host, port)
47 | host = host[0, 0xFF]
48 | socket << [VERSION, 0x01, 0x00, 0x03, host.size, host, port].pack("CCCCCA#{host.size}n")
49 | version, status, _, type = socket.read(4).unpack("CCCC")
50 | check_version(version)
51 |
52 | case status
53 | when 0x00 # succeeded
54 | when 0x01 # general SOCKS server failure
55 | raise "general SOCKS server failure"
56 | when 0x02 # connection not allowed by ruleset
57 | raise "connection not allowed by ruleset"
58 | when 0x03 # Network unreachable
59 | raise "network unreachable"
60 | when 0x04 # Host unreachable
61 | raise "host unreachable"
62 | when 0x05 # Connection refused
63 | raise "connection refused"
64 | when 0x06 # TTL expired
65 | raise "TTL expired"
66 | when 0x07 # Command not supported
67 | raise "command not supported"
68 | when 0x08 # Address type not supported
69 | raise "address type not supported"
70 | else # unassigned
71 | raise "unknown SOCKS error"
72 | end
73 |
74 | case type
75 | when 0x01 # IP V4 address
76 | destination = IPAddr.ntop(socket.read(4))
77 | when 0x03 # DOMAINNAME
78 | length = socket.read(1).unpack("C").first
79 | destination = socket.read(length).unpack("A#{length}")
80 | when 0x04 # IP V6 address
81 | destination = IPAddr.ntop(socket.read(16))
82 | else
83 | raise "unsupported SOCKS5 address type"
84 | end
85 |
86 | port = socket.read(2).unpack("n").first
87 | end
88 |
89 | def check_version(version, should_be = VERSION)
90 | raise "mismatched SOCKS version" unless version == should_be
91 | end
92 |
93 | private
94 | def authentication_methods
95 | methods = []
96 | methods << 0x00 # NO AUTHENTICATION REQUIRED
97 | methods << 0x02 if user # USERNAME/PASSWORD
98 | methods
99 | end
100 | end
101 |
102 | SOCKS5Proxy = SOCKSProxy
103 | end
104 |
--------------------------------------------------------------------------------
/lib/proxifier/proxies/socks4.rb:
--------------------------------------------------------------------------------
1 | require "proxifier/proxies/socks"
2 |
3 | module Proxifier
4 | class SOCKS4Proxy < SOCKSProxy
5 | VERSION = 0x04
6 |
7 | protected
8 | def greet(socket)
9 | # noop
10 | end
11 |
12 | def authenticate(socket, method)
13 | # noop
14 | end
15 |
16 | def connect(socket, host, port)
17 | begin
18 | ip = IPAddr.new(host)
19 | rescue ArgumentError
20 | ip = IPAddr.new(Socket.getaddrinfo(host, nil, :INET, :STREAM).first)
21 | end
22 |
23 | socket << [VERSION, 0x01, port].pack("CCn") << ip.hton
24 | socket << user if user
25 | socket << 0x00
26 |
27 | version, status, port = socket.read(4).unpack("CCn")
28 | check_version(version, 0x00)
29 | ip = IPAddr.ntop(socket.read(4))
30 |
31 | case status
32 | when 0x5A # request granted
33 | when 0x5B # request rejected or failed
34 | raise "request rejected or failed"
35 | when 0x5C # request rejected becasue SOCKS server cannot connect to identd on the client
36 | raise "request rejected becasue SOCKS server cannot connect to identd on the client"
37 | when 0x5D # request rejected because the client program and identd report different user-ids
38 | raise "request rejected because the client program and identd report different user-ids"
39 | else
40 | raise "unknown SOCKS error"
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/proxifier/proxies/socks4a.rb:
--------------------------------------------------------------------------------
1 | require "proxifier/proxies/socks"
2 |
3 | module Proxifier
4 | class SOCKS4AProxy < SOCKSProxy
5 | def do_proxify(*)
6 | raise NotImplementedError, "SOCKS4A is not yet implemented"
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/proxifier/proxy.rb:
--------------------------------------------------------------------------------
1 | require "socket"
2 | require "uri"
3 | require "uri/socks"
4 |
5 | module Proxifier
6 | class Proxy
7 | class << self
8 | def proxify?(host, no_proxy = nil)
9 | return true unless no_proxy
10 |
11 | dont_proxy = no_proxy.split(",")
12 | dont_proxy.none? { |h| host =~ /#{h}\Z/ }
13 | end
14 | end
15 |
16 | attr_reader :url, :options
17 |
18 | def initialize(url, options = {})
19 | url = URI.parse(uri) unless url.is_a?(URI::Generic)
20 | @url, @options = url, options
21 | end
22 |
23 | def open(host, port, local_host = nil, local_port = nil)
24 | return TCPSocket.new(host, port, local_host, local_port) unless proxify?(host)
25 |
26 | socket = TCPSocket.new(self.host, self.port, local_host, local_port)
27 |
28 | begin
29 | proxify(socket, host, port)
30 | rescue
31 | socket.close
32 | raise
33 | end
34 |
35 | socket
36 | end
37 |
38 | def proxify?(host)
39 | self.class.proxify?(host, options[:no_proxy])
40 | end
41 |
42 | def proxify(socket, host, port)
43 | do_proxify(socket, host, port)
44 | end
45 |
46 | %w(host port user password query version).each do |attr|
47 | class_eval "def #{attr}; url.#{attr} end"
48 | end
49 |
50 | def query_options
51 | @query_options ||= query ? Hash[query.split("&").map { |q| q.split("=") }] : {}
52 | end
53 |
54 | %w(no_proxy).each do |option|
55 | class_eval "def #{option}; options[:#{option}] end"
56 | end
57 |
58 | protected
59 | def do_proxify(socket, host, port)
60 | raise NotImplementedError, "#{self} must implement do_proxify"
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/proxifier/version.rb:
--------------------------------------------------------------------------------
1 | module Proxifier
2 | VERSION = "1.0.3"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/uri/socks.rb:
--------------------------------------------------------------------------------
1 | require "uri/generic"
2 |
3 | module URI
4 | class SOCKS < Generic
5 | DEFAULT_PORT = 1080
6 | COMPONENT = [:scheme, :userinfo, :host, :port, :query].freeze
7 | end
8 | @@schemes["SOCKS"] = SOCKS
9 | @@schemes["SOCKS5"] = SOCKS
10 |
11 | class SOCKS4 < SOCKS
12 | end
13 | @@schemes["SOCKS4"] = SOCKS4
14 |
15 | class SOCKS4A < SOCKS
16 | end
17 | @@schemes["SOCKS4A"] = SOCKS4A
18 | end
19 |
--------------------------------------------------------------------------------
/proxifier.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 | require "proxifier/version"
3 |
4 | Gem::Specification.new do |s|
5 | s.name = "proxifier"
6 | s.version = Proxifier::VERSION
7 | s.platform = Gem::Platform::RUBY
8 | s.authors = ["Samuel Kadolph"]
9 | s.email = ["samuel@kadolph.com"]
10 | s.homepage = "https://github.com/samuelkadolph/ruby-proxifier"
11 | s.summary = %q{Proxifier is a gem to force ruby to use a proxy.}
12 | s.description = %q{Proxifier adds support for HTTP or SOCKS proxies and lets you force TCPSocket to use proxies.}
13 |
14 | s.files = Dir["bin/*", "lib/**/*"] + ["LICENSE", "README.md"]
15 | s.executables = ["pirb", "pruby"]
16 | end
17 |
--------------------------------------------------------------------------------