├── .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 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Variable NameAlternativesNotes
proxyPROXYRequires the proxy scheme to be present.
socks_proxySOCKS_PROXY
socks5_proxy
SOCKS5_PROXY
Implies the SOCKS5 proxy scheme.
socks4a_proxySOCKS4A_PROXYImplies the SOCKS4A proxy scheme.
socks4_proxyPROXYImplies the SOCKS4 proxy scheme.
http_proxyHTTP_PROXYImplies the HTTP proxy scheme.
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 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 104 | 106 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
ProtocolFormatsNotes
HTTP
http://[username[:password]@]host[:port][?tunnel=false]
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.
SOCKS5
socks://[username[:password]@]host[:port]
105 | socks5://[username[:password]@]host[:port]
107 | Port defaults to 1080. 108 |
SOCKS4A
socks4a://[username@]host[:port]
Not yet implemented.
SOCKS4
socks4://[username@]host[:port]
Currently hangs. Not sure if the problem is with code or server.
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 | --------------------------------------------------------------------------------