Initial version

This commit is contained in:
Samuel Kadolph 2011-06-09 11:28:05 -04:00
commit 0be892763b
19 changed files with 535 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.gem
.bundle
Gemfile.lock
pkg/*

3
Gemfile Normal file
View File

@ -0,0 +1,3 @@
source "http://rubygems.org"
gemspec

19
LICENSE Normal file
View File

@ -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.

89
README.md Normal file
View File

@ -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
<table>
<tr>
<th>Protocol</th>
<th>Formats</th>
<th>Notes</th>
</tr>
<tr>
<td>HTTP</td>
<td>
```
http://[username[:password]@]host[:port][?tunnel=false]
```
</td>
<td>
The port defaults to 80. This is currently a limitation that may be solved in the future.<br>
Appending <code>?tunnel=false</code> forces the proxy to not use <code>CONNECT</code>.</td>
</tr>
<tr>
<td>SOCKS5</td>
<td>
```
socks://[username[:password]@]host[:port]
socks5://[username[:password]@]host[:port]
```
</td>
<td>
Port defaults to 1080.
</td>
</tr>
<tr>
<td>SOCKS4</td>
<td>
```
socks4://[username@]ip1.ip2.ip3.ip4[:port]
```
</td>
<td>Currently hangs. Not sure if the problem is with code or server.</td>
</tr>
<tr>
<td>SOCKS4A</td>
<td>
```
socks4a://[username@]host[:port]
```
</td>
<td>Not yet implemented.</td>
</tr>
</table>

1
Rakefile Normal file
View File

@ -0,0 +1 @@
require "bundler/gem_tasks"

7
bin/pirb Executable file
View File

@ -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)

7
bin/pruby Executable file
View File

@ -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)

5
lib/sockets.rb Normal file
View File

@ -0,0 +1,5 @@
require "sockets/version"
require "sockets/proxy"
module Sockets
end

28
lib/sockets/env.rb Normal file
View File

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

4
lib/sockets/proxies.rb Normal file
View File

@ -0,0 +1,4 @@
module Sockets
module Proxies
end
end

View File

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

View File

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

View File

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

View File

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

77
lib/sockets/proxify.rb Normal file
View File

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

75
lib/sockets/proxy.rb Normal file
View File

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

3
lib/sockets/version.rb Normal file
View File

@ -0,0 +1,3 @@
module Sockets
VERSION = "1.0.0"
end

18
lib/uri/socks.rb Normal file
View File

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

16
sockets.gemspec Normal file
View File

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