em-socksify-0.3.0/0000755000004100000410000000000012250222462013770 5ustar www-datawww-dataem-socksify-0.3.0/Rakefile0000644000004100000410000000022012250222462015427 0ustar www-datawww-datarequire 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' desc "Run all RSpec tests" RSpec::Core::RakeTask.new(:spec) em-socksify-0.3.0/Gemfile0000644000004100000410000000004612250222462015263 0ustar www-datawww-datasource "http://rubygems.org" gemspec em-socksify-0.3.0/spec/0000755000004100000410000000000012250222462014722 5ustar www-datawww-dataem-socksify-0.3.0/spec/helper.rb0000644000004100000410000000010112250222462016516 0ustar www-datawww-datarequire 'rubygems' require 'bundler/setup' require 'em-socksify'em-socksify-0.3.0/spec/socksify_spec.rb0000644000004100000410000000252012250222462020112 0ustar www-datawww-datarequire 'helper' describe EventMachine do # requires: ssh -D 8080 localhost it "should negotiate a socks connection" do class Handler < EM::Connection include EM::Socksify def connection_completed socksify('google.com', 80) do |ip| send_data "GET / HTTP/1.1\r\nConnection:close\r\nHost: google.com\r\n\r\n" end end def receive_data(data) @received ||= '' @received << data end def unbind @received.size.should > 0 @received[0,4].should == 'HTTP' EM.stop end end EM.run do EventMachine.connect '127.0.0.1', 8080, Handler end end # requires squid running on localhost with default config it "should negotiate a connect connection" do class Handler < EM::Connection include EM::Connectify def connection_completed connectify('www.google.com', 443) do start_tls send_data "GET / HTTP/1.1\r\nConnection:close\r\nHost: www.google.com\r\n\r\n" end end def receive_data(data) @received ||= '' @received << data end def unbind @received.size.should > 0 @received[0,4].should == 'HTTP' EM.stop end end EM::run do EventMachine.connect '127.0.0.1', 8081, Handler end end end em-socksify-0.3.0/lib/0000755000004100000410000000000012250222462014536 5ustar www-datawww-dataem-socksify-0.3.0/lib/em-socksify/0000755000004100000410000000000012250222462016767 5ustar www-datawww-dataem-socksify-0.3.0/lib/em-socksify/socks5.rb0000644000004100000410000000702412250222462020526 0ustar www-datawww-datamodule EventMachine module Socksify module SOCKS5 def socks_send_handshake # Method Negotiation as described on # http://www.faqs.org/rfcs/rfc1928.html Section 3 @socks_state = :method_negotiation socks_methods.tap do |methods| send_data [5, methods.size].pack('CC') + methods.pack('C*') end end def socks_send_connect_request @socks_state = :connecting send_data [5, 1, 0].pack('CCC') if matches = @socks_target_host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) send_data "\x01" + matches.to_a[1 .. -1].map { |s| s.to_i }.pack('CCCC') elsif @socks_target_host =~ /^[:0-9a-f]+$/ raise SOCKSError, 'TCP/IPv6 over SOCKS is not yet supported (inet_pton missing in Ruby & not supported by Tor)' else send_data [3, @socks_target_host.size, @socks_target_host].pack('CCA*') end send_data [@socks_target_port].pack('n') end def socks_send_authentication @socks_state = :authenticating send_data [5, @socks_username.length, @socks_username, @socks_password.length, @socks_password ].pack('CCA*CA*') end private # parses socks 5 server responses as specified # on http://www.faqs.org/rfcs/rfc1928.html def socks_parse_response case @socks_state when :method_negotiation return unless @socks_data.size >= 2 _, method = @socks_data.slice!(0, 2).unpack('CC') if socks_methods.include?(method) case method when 0 then socks_send_connect_request when 2 then socks_send_authentication end else raise SOCKSError, 'proxy did not accept method' end when :authenticating return unless @socks_data.size >= 2 socks_version, status_code = @socks_data.slice!(0, 2).unpack('CC') raise SOCKSError, "SOCKS version 5 not supported" unless socks_version == 5 raise SOCKSError, 'access denied by proxy' unless status_code == 0 send_socks_connect_request when :connecting return unless @socks_data.size >= 2 socks_version, status_code = @socks_data.slice(0, 2).unpack('CC') raise SOCKSError, "SOCKS version #{socks_version} is not 5" unless socks_version == 5 raise SOCKSError.for_response_code(status_code) unless status_code == 0 min_size = @socks_data[3].ord == 3 ? 5 : 4 return unless @socks_data.size >= min_size size = case @socks_data[3].ord when 1 then 4 when 3 then @socks_data[4].ord when 4 then 16 else raise SOCKSError.for_response_code(@socks_data[3]) end return unless @socks_data.size >= (min_size + size) bind_addr = @socks_data.slice(min_size, size) ip = case @socks_data[3].ord when 1 then bind_addr.bytes.to_a.join('.') when 3 then bind_addr when 4 then # TODO: ??? end socks_unhook(ip) end rescue Exception => e @socks_deferrable.fail(e) end def socks_methods methods = [] methods << 2 if !@socks_username.nil? # 2 => Username/Password Authentication methods << 0 # 0 => No Authentication Required methods end end end end em-socksify-0.3.0/lib/em-socksify/errors.rb0000644000004100000410000000234012250222462020627 0ustar www-datawww-datamodule EventMachine module Socksify class SOCKSError < Exception def self.define (message) Class.new(self) do def initialize super(message) end end end ServerFailure = define('general SOCKS server failure') NotAllowed = define('connection not allowed by ruleset') NetworkUnreachable = define('Network unreachable') HostUnreachable = define('Host unreachable') ConnectionRefused = define('Connection refused') TTLExpired = define('TTL expired') CommandNotSupported = define('Command not supported') AddressTypeNotSupported = define('Address type not supported') def self.for_response_code(code) case code.is_a?(String) ? code.ord : code when 1 then ServerFailure when 2 then NotAllowed when 3 then NetworkUnreachable when 4 then HostUnreachable when 5 then ConnectionRefused when 6 then TTLExpired when 7 then CommandNotSupported when 8 then AddressTypeNotSupported else self end end end end module Connectify class CONNECTError < Exception end end end em-socksify-0.3.0/lib/em-socksify/version.rb0000644000004100000410000000010612250222462020776 0ustar www-datawww-datamodule EventMachine module Socksify VERSION = "0.3.0" end end em-socksify-0.3.0/lib/em-socksify/connect.rb0000644000004100000410000000143112250222462020744 0ustar www-datawww-datamodule EventMachine module Connectify module CONNECT def connect_send_handshake header = "CONNECT #{@connect_target_host}:#{@connect_target_port} HTTP/1.0\r\n" if @connect_username || @connect_password encoded_credentials = Base64.strict_encode64([@connect_username, @connect_password].join(":")) header << "Proxy-Authorization: Basic #{encoded_credentials}\r\n" end header << "\r\n" send_data(header) end private def connect_parse_response unless @connect_data =~ %r{\AHTTP/1\.[01] 200 .*\r\n\r\n}m raise CONNECTError.new, "Unexpected response: #{@connect_data}" end connect_unhook rescue => e @connect_deferrable.fail e end end end end em-socksify-0.3.0/lib/em-socksify/socksify.rb0000644000004100000410000000167612250222462021160 0ustar www-datawww-datamodule EventMachine module Socksify def socksify(host, port, username = nil, password = nil, version = 5, &blk) @socks_target_host = host @socks_target_port = port @socks_username = username @socks_password = password @socks_version = version @socks_data = '' socks_hook socks_send_handshake @socks_deferrable = DefaultDeferrable.new @socks_deferrable.callback(&blk) if blk @socks_deferrable end def socks_hook if @socks_version == 5 extend SOCKS5 else raise ArgumentError, 'SOCKS version unsupported' end class << self alias receive_data socks_receive_data end end def socks_unhook(ip = nil) class << self remove_method :receive_data end @socks_deferrable.succeed(ip) end def socks_receive_data(data) @socks_data << data socks_parse_response end end end em-socksify-0.3.0/lib/em-socksify/connectify.rb0000644000004100000410000000147212250222462021461 0ustar www-datawww-datamodule EventMachine module Connectify def connectify(host, port, username=nil, password=nil, &blk) @connect_target_host = host @connect_target_port = port @connect_username = username @connect_password = password @connect_data = '' connect_hook connect_send_handshake @connect_deferrable = DefaultDeferrable.new @connect_deferrable.callback(&blk) if blk @connect_deferrable end def connect_hook extend CONNECT class << self alias receive_data connect_receive_data end end def connect_unhook class << self remove_method :receive_data end @connect_deferrable.succeed end def connect_receive_data(data) @connect_data << data connect_parse_response end end end em-socksify-0.3.0/lib/em-socksify.rb0000644000004100000410000000062112250222462017313 0ustar www-datawww-datarequire 'eventmachine' require 'base64' require 'em-socksify/socksify' require 'em-socksify/errors' require 'em-socksify/socks5' require 'em-socksify/connectify' require 'em-socksify/connect' # Backport from ruby-1.9 to ruby-1.8 (which doesn't support pack('m0') either) unless Base64.respond_to?(:strict_encode64) def Base64.strict_encode64(str) Base64.encode64(str).gsub("\n", "") end end em-socksify-0.3.0/metadata.yml0000644000004100000410000000501712250222462016276 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: em-socksify version: !ruby/object:Gem::Version version: 0.3.0 prerelease: platform: ruby authors: - Ilya Grigorik autorequire: bindir: bin cert_chain: [] date: 2013-06-17 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: eventmachine requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.0.0.beta.4 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 1.0.0.beta.4 - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' description: Transparent proxy support for any EventMachine protocol email: - ilya@igvita.com executables: [] extensions: [] extra_rdoc_files: [] files: - .gitignore - Gemfile - README.md - Rakefile - em-socksify.gemspec - lib/em-socksify.rb - lib/em-socksify/connect.rb - lib/em-socksify/connectify.rb - lib/em-socksify/errors.rb - lib/em-socksify/socks5.rb - lib/em-socksify/socksify.rb - lib/em-socksify/version.rb - spec/helper.rb - spec/socksify_spec.rb homepage: http://github.com/igrigorik/em-socksify licenses: [] post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: em-socksify rubygems_version: 1.8.24 signing_key: specification_version: 3 summary: Transparent proxy support for any EventMachine protocol test_files: - spec/helper.rb - spec/socksify_spec.rb has_rdoc: em-socksify-0.3.0/.gitignore0000644000004100000410000000004112250222462015753 0ustar www-datawww-datapkg/* *.gem .bundle Gemfile.lock em-socksify-0.3.0/em-socksify.gemspec0000644000004100000410000000160112250222462017564 0ustar www-datawww-data# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "em-socksify/version" Gem::Specification.new do |s| s.name = "em-socksify" s.version = EventMachine::Socksify::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Ilya Grigorik"] s.email = ["ilya@igvita.com"] s.homepage = "http://github.com/igrigorik/em-socksify" s.summary = "Transparent proxy support for any EventMachine protocol" s.description = s.summary s.rubyforge_project = "em-socksify" s.add_dependency "eventmachine", ">= 1.0.0.beta.4" s.add_development_dependency "rspec" s.add_development_dependency "rake" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] end em-socksify-0.3.0/README.md0000644000004100000410000000443212250222462015252 0ustar www-datawww-data# Transparent proxy support for any EventMachine protocol Dealing with SOCKS and HTTP proxies is a pain. EM-Socksify provides a simple ship to setup and negotiation a SOCKS / HTTP connection for any EventMachine protocol. ### Example: Routing HTTP request via SOCKS5 proxy ```ruby class Handler < EM::Connection include EM::Socksify def connection_completed socksify('google.ca', 80) do send_data "GET / HTTP/1.1\r\nConnection:close\r\nHost: google.ca\r\n\r\n" end end def receive_data(data) p data end end EM.run do EventMachine.connect SOCKS_HOST, SOCKS_PORT, Handler end ``` What's happening here? First, we open a raw TCP connection to the SOCKS proxy. Once the TCP connection is established, EventMachine calls the **connection_completed** method in our handler, at which point we call the helper method (**socksify**) with the actual destination and host and port (address that we actually want to get to), and the module does the rest. socksify temporarily intercepts your receive_data callbacks, negotiates the SOCKS connection (version, authentication, etc), and then once all of that is done, returns control back to your code. For SOCKS proxies which require authentication, use: ```ruby socksify(destination_host, destination_port, username, password, version) ``` ### Example: Routing HTTPS request via a squid CONNECT proxy ```ruby class Handler < EM::Connection include EM::Connectify def connection_completed connectify('www.google.ca', 443) do start_tls send_data "GET / HTTP/1.1\r\nConnection:close\r\nHost: www.google.ca\r\n\r\n" end end def receive_data(data) p data end end EM.run do EventMachine.connect PROXY_HOST, PROXY_PORT, Handler end ``` For CONNECT proxies which require authentication, use: ```ruby connectify(destination_host, destination_port, username, password) ``` ### Wishlist - IPV6 support - SOCKS4 support ### Resources - [SOCKS on Wikipedia](http://en.wikipedia.org/wiki/SOCKS) - [Socksify-Ruby](https://github.com/astro/socksify-ruby) for regular Ruby TCPSocket - [HTTP Connect Tunneling](http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling) ### Contributors - [Ilya Grigorik](https://github.com/igrigorik) - [Conrad Irwin](https://github.com/ConradIrwin) ### License (The MIT License)