commit 0be892763b8c5e51198615a92fea8c9ad713a144 Author: Samuel Kadolph Date: Thu Jun 9 11:28:05 2011 -0400 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4040c6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.gem +.bundle +Gemfile.lock +pkg/* diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c80ee36 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "http://rubygems.org" + +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cbe53cc --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2011 by Samuel Kadolph + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebd23eb --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# ruby-sockets + +## Installing + +### Recommended + +``` +gem install sockets +``` + +### Edge + +``` +git clone https://github.com/samuelkadolph/ruby-sockets +cd ruby-sockets && rake install +``` + +## Usage + +### Environment Variables & Executable Wrappers + +sockets provides two executables: `pruby` and `pirb`. They are simple wrappers +for your current `ruby` and `irb` executables that `require "sockets/env"` +which installs hooks to `TCPSocket` which will use your proxy environment +variables whenever a `TCPSocket` is created. sockets will use the +`proxy`, `PROXY`, `socks_proxy` and `http_proxy` environment variables (in that +order) to determine what proxy to use. + +### Ruby + +```ruby +require "sockets/proxy" + +proxy = Sockets::Proxy("socks://localhost") +socket = proxy.open("www.google.com", 80) +socket << "GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n" +socket.gets # => "HTTP/1.1 200 OK\r\n" +``` + +## Supported Proxies + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProtocolFormatsNotes
HTTP +``` +http://[username[:password]@]host[:port][?tunnel=false] +``` + + The port defaults to 80. This is currently a limitation that may be solved in the future.
+ Appending ?tunnel=false forces the proxy to not use CONNECT.
SOCKS5 +``` +socks://[username[:password]@]host[:port] +socks5://[username[:password]@]host[:port] +``` + + Port defaults to 1080. +
SOCKS4 +``` +socks4://[username@]ip1.ip2.ip3.ip4[:port] +``` + Currently hangs. Not sure if the problem is with code or server.
SOCKS4A +``` +socks4a://[username@]host[:port] +``` + Not yet implemented.
diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/bin/pirb b/bin/pirb new file mode 100755 index 0000000..7201000 --- /dev/null +++ b/bin/pirb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +executable = File.expand_path("../" + Gem.default_exec_format % "irb", Gem.ruby) +load_paths = Gem.loaded_specs["sockets"].load_paths.map { |p| "-I#{p}" } +# TODO: support argument switches + +exec(executable, *load_paths, "-rsockets/env", *ARGV) diff --git a/bin/pruby b/bin/pruby new file mode 100755 index 0000000..12fe0e2 --- /dev/null +++ b/bin/pruby @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +executable = Gem.ruby +load_paths = Gem.loaded_specs["sockets"].load_paths.map { |p| "-I#{p}" } +# TODO: support argument switches + +exec(executable, *load_paths, "-rsockets/env", *ARGV) diff --git a/lib/sockets.rb b/lib/sockets.rb new file mode 100644 index 0000000..9457c4b --- /dev/null +++ b/lib/sockets.rb @@ -0,0 +1,5 @@ +require "sockets/version" +require "sockets/proxy" + +module Sockets +end diff --git a/lib/sockets/env.rb b/lib/sockets/env.rb new file mode 100644 index 0000000..12ac9ab --- /dev/null +++ b/lib/sockets/env.rb @@ -0,0 +1,28 @@ +require "socket" + +require "sockets" +require "sockets/proxify" + +module Sockets + class Proxy + def open(host, port, local_host = nil, local_port = nil) + if proxify?(host) + socket = TCPSocket.new(proxy.host, proxy.port, local_host, local_port, :proxy => nil) + begin + proxify(socket, host, port) + rescue Exception + socket.close + raise + end + socket + else + TCPSocket.new(host, port, local_host, local_port, :proxy => nil) + end + end + end +end + +class TCPSocket + include Sockets::Proxify + include Sockets::EnvironmentProxify +end diff --git a/lib/sockets/proxies.rb b/lib/sockets/proxies.rb new file mode 100644 index 0000000..8f12340 --- /dev/null +++ b/lib/sockets/proxies.rb @@ -0,0 +1,4 @@ +module Sockets + module Proxies + end +end diff --git a/lib/sockets/proxies/http.rb b/lib/sockets/proxies/http.rb new file mode 100644 index 0000000..0a886b9 --- /dev/null +++ b/lib/sockets/proxies/http.rb @@ -0,0 +1,21 @@ +require "net/http" +require "sockets/proxy" + +module Sockets + module Proxies + class HTTP < Proxy + def do_proxify(socket, host, port) + return if query_options["tunnel"] == "false" + + socket << "CONNECT #{host}:#{port} HTTP/1.1\r\n" + socket << "Host: #{host}:#{port}\r\n" + socket << "Proxy-Authorization: Basic #{["#{user}:#{password}"].pack("m").chomp}\r\n" if user + socket << "\r\n" + + buffer = Net::BufferedIO.new(socket) + response = Net::HTTPResponse.read_new(buffer) + response.error! unless response.is_a?(Net::HTTPOK) + end + end + end +end diff --git a/lib/sockets/proxies/socks.rb b/lib/sockets/proxies/socks.rb new file mode 100644 index 0000000..c571ac9 --- /dev/null +++ b/lib/sockets/proxies/socks.rb @@ -0,0 +1,103 @@ +require "ipaddr" +require "sockets/proxy" + +module Sockets + module Proxies + class SOCKS < Proxy + VERSION = 0x05 + + def do_proxify(socket, host, port) + authenticaton_method = greet(socket) + authenticate(socket, authenticaton_method) + connect(socket, host, port) + end + + protected + def greet(socket) + methods = authentication_methods + + socket << [VERSION, methods.size, *methods].pack("CCC#{methods.size}") + version, authentication_method = socket.read(2).unpack("CC") + check_version(version) + + authentication_method + end + + def authenticate(socket, method) + case method + when 0x00 # NO AUTHENTICATION REQUIRED + when 0x02 # USERNAME/PASSWORD + user &&= user[0, 0xFF] + password &&= password[0, 0xFF] + + socket << [user.size, user, password.size, password].pack("CA#{user.size}CA#{password.size}") + version, status = socket.read(2).unpack("CC") + check_version(version) + + case status + when 0x00 # SUCCESS + else + raise "SOCKS5 username/password authentication failed" + end + else + raise "no acceptable SOCKS5 authentication methods" + end + end + + def connect(socket, host, port) + host = host[0, 0xFF] + socket << [VERSION, 0x01, 0x00, 0x03, host.size, host, port].pack("CCCCCA#{host.size}n") + version, status, _, type = socket.read(4).unpack("CCCC") + check_version(version) + + case status + when 0x00 # succeeded + when 0x01 # general SOCKS server failure + raise "general SOCKS server failure" + when 0x02 # connection not allowed by ruleset + raise "connection not allowed by ruleset" + when 0x03 # Network unreachable + raise "network unreachable" + when 0x04 # Host unreachable + raise "host unreachable" + when 0x05 # Connection refused + raise "connection refused" + when 0x06 # TTL expired + raise "TTL expired" + when 0x07 # Command not supported + raise "command not supported" + when 0x08 # Address type not supported + raise "address type not supported" + else # unassigned + raise "unknown SOCKS error" + end + + case type + when 0x01 # IP V4 address + destination = IPAddr.ntop(socket.read(4)) + when 0x03 # DOMAINNAME + length = socket.read(1).unpack("C").first + destination = socket.read(length).unpack("A#{length}") + when 0x04 # IP V6 address + destination = IPAddr.ntop(socket.read(16)) + else + raise "unsupported SOCKS5 address type" + end + + port = socket.read(2).unpack("n").first + end + + def check_version(version, should_be = VERSION) + raise "mismatched SOCKS version" unless version == should_be + end + + private + def authentication_methods + methods = [] + methods << 0x00 # NO AUTHENTICATION REQUIRED + methods << 0x02 if user # USERNAME/PASSWORD + methods + end + end + end +end diff --git a/lib/sockets/proxies/socks4.rb b/lib/sockets/proxies/socks4.rb new file mode 100644 index 0000000..f6af9e0 --- /dev/null +++ b/lib/sockets/proxies/socks4.rb @@ -0,0 +1,46 @@ +require "sockets/proxies/socks" + +module Sockets + module Proxies + class SOCKS4 < SOCKS + VERSION = 0x04 + + protected + def greet(socket) + # noop + end + + def authenticate(socket, method) + # noop + end + + def connect(socket, host, port) + begin + ip = IPAddr.new(host) + rescue ArgumentError + ip = IPAddr.new(Socket.getaddrinfo(host, nil, :INET, :STREAM).first) + end + + socket << [VERSION, 0x01, port].pack("CCn") << ip.hton + socket << user if user + socket << 0x00 + + version, status, port = socket.read(4).unpack("CCn") + check_version(version, 0x00) + ip = IPAddr.ntop(socket.read(4)) + + case status + when 0x5A # request granted + when 0x5B # request rejected or failed + raise "request rejected or failed" + when 0x5C # request rejected becasue SOCKS server cannot connect to identd on the client + raise "request rejected becasue SOCKS server cannot connect to identd on the client" + when 0x5D # request rejected because the client program and identd report different user-ids + raise "request rejected because the client program and identd report different user-ids" + else + raise "unknown SOCKS error" + end + end + end + end +end diff --git a/lib/sockets/proxies/socks4a.rb b/lib/sockets/proxies/socks4a.rb new file mode 100644 index 0000000..357d1f4 --- /dev/null +++ b/lib/sockets/proxies/socks4a.rb @@ -0,0 +1,9 @@ +module Sockets + module Proxies + class SOCKS4A < Proxy + def do_proxify(*) + raise NotImplementedError, "SOCKS4A is not yet implemented" + end + end + end +end diff --git a/lib/sockets/proxify.rb b/lib/sockets/proxify.rb new file mode 100644 index 0000000..271091f --- /dev/null +++ b/lib/sockets/proxify.rb @@ -0,0 +1,77 @@ +require "sockets/proxy" + +module Sockets + module Proxify + def self.included(klass) + klass.class_eval do + alias_method :initialize_without_proxy, :initialize + alias_method :initialize, :initialize_with_proxy + end + end + + def initialize_with_proxy(host, port, options_or_local_host = {}, local_port = nil, options_if_local_host = {}) + if options_or_local_host.is_a?(Hash) + local_host = nil + options = options_or_local_host + else + local_host = options_or_local_host + options = options_if_local_host + end + + if options[:proxy] && (proxy = Sockets::Proxy(options.delete(:proxy), options)) && proxy.proxify?(host) + initialize_without_proxy(proxy.host, proxy.port, local_host, local_port) + begin + proxy.proxify(self, host, port) + rescue Exception + close + raise + end + else + initialize_without_proxy(host, port, local_host, local_port) + end + end + end +end + +module Sockets + module EnvironmentProxify + def self.included(klass) + klass.class_eval do + extend ClassMethods + alias_method :initialize_without_environment_proxy, :initialize + alias_method :initialize, :initialize_with_environment_proxy + end + end + + def initialize_with_environment_proxy(host, port, options_or_local_host = {}, local_port = nil, options_if_local_host = {}) + if options_or_local_host.is_a?(Hash) + local_host = nil + options = options_or_local_host + else + local_host = options_or_local_host + options = options_if_local_host + end + + options = { :proxy => environment_proxy, :no_proxy => environment_no_proxy }.merge(options) + initialize_without_environment_proxy(host, port, local_host, local_port, options) + end + + def environment_proxy + self.class.environment_proxy + end + + def environment_no_proxy + self.class.environment_no_proxy + end + + module ClassMethods + def environment_proxy + ENV["proxy"] || ENV["PROXY"] || ENV["socks_proxy"] || ENV["http_proxy"] + end + + def environment_no_proxy + ENV["no_proxy"] + end + end + end +end diff --git a/lib/sockets/proxy.rb b/lib/sockets/proxy.rb new file mode 100644 index 0000000..6acaff1 --- /dev/null +++ b/lib/sockets/proxy.rb @@ -0,0 +1,75 @@ +require "socket" +require "uri" +require "uri/socks" + +module Sockets + class Proxy + attr_reader :url, :options + + def initialize(url, options = {}) + url = URI.parse(uri) unless url.is_a?(URI::Generic) + @url, @options = url, options + end + + def open(host, port, local_host = nil, local_port = nil) + if proxify?(host) + socket = TCPSocket.new(proxy.host, proxy.port, local_host, local_port) + begin + proxify(socket, host, port) + rescue Exception + socket.close + raise + end + socket + else + TCPSocket.new(host, port, local_host, local_port) + end + end + + def proxify?(host) + return true unless options[:no_proxy] + + dont_proxy = options[:no_proxy].split(",") + dont_proxy.none? { |h| host =~ /#{h}\Z/ } + end + + def proxify(socket, host, port) + do_proxify(socket, host, port) + end + + %w(host port user password query version).each do |attr| + class_eval "def #{attr}; url.#{attr} end", __FILE__, __LINE__ + end + + def query_options + @query ||= query ? Hash[query.split("&").map { |q| q.split("=") }] : {} + end + + %w(no_proxy).each do |option| + class_eval "def #{option}; options[:#{option}] end", __FILE__, __LINE__ + end + + protected + def do_proxify(socket, host, port) + raise NotImplementedError, "#{self} must implement do_proxify" + end + end + + def self.Proxy(url, options = {}) + url = URI.parse(url) + + raise(ArgumentError, "proxy has no scheme") unless url.scheme + begin + klass = Proxies.const_get(url.scheme.upcase) + rescue NameError + begin + require "sockets/proxies/#{url.scheme}" + klass = Proxies.const_get(url.scheme.upcase) + rescue LoadError, NameError + raise(ArgumentError, "unknown proxy scheme `#{url.scheme}'") + end + end + + klass.new(url, options) + end +end diff --git a/lib/sockets/version.rb b/lib/sockets/version.rb new file mode 100644 index 0000000..ba03ae5 --- /dev/null +++ b/lib/sockets/version.rb @@ -0,0 +1,3 @@ +module Sockets + VERSION = "1.0.0" +end diff --git a/lib/uri/socks.rb b/lib/uri/socks.rb new file mode 100644 index 0000000..4cf050b --- /dev/null +++ b/lib/uri/socks.rb @@ -0,0 +1,18 @@ +require "uri/generic" + +module URI + class SOCKS < Generic + DEFAULT_PORT = 1080 + COMPONENT = [:scheme, :userinfo, :host, :port, :query].freeze + end + @@schemes["SOCKS"] = SOCKS + @@schemes["SOCKS5"] = SOCKS + + class SOCKS4 < SOCKS + end + @@schemes["SOCKS4"] = SOCKS4 + + class SOCKS4A < SOCKS + end + @@schemes["SOCKS4A"] = SOCKS4A +end diff --git a/sockets.gemspec b/sockets.gemspec new file mode 100644 index 0000000..b6cb5d7 --- /dev/null +++ b/sockets.gemspec @@ -0,0 +1,16 @@ +$:.push File.expand_path("../lib", __FILE__) +require "sockets/version" + +Gem::Specification.new do |s| + s.name = "sockets" + s.version = Sockets::VERSION + s.platform = Gem::Platform::RUBY + s.authors = ["Samuel Kadolph"] + s.email = ["samuel@kadolph.com"] + s.homepage = "https://github.com/samuelkadolph/ruby-sockets" + s.summary = %q{} + s.description = %q{} + + s.files = Dir["{bin,lib}/**/*"] + ["LICENSE", "README.md"] + s.executables = ["pruby", "pirb"] +end