From f1d370079c4a7d7e998832a03f9b0fecb9144f0a Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Sat, 23 Sep 2017 04:19:47 +0900 Subject: [PATCH 01/10] - implemented socks proxy functionality --- README.md | 2 +- lib/httpclient.rb | 14 +- lib/httpclient/session.rb | 42 +++++ lib/httpclient/socks.rb | 213 +++++++++++++++++++++ lib/httpclient/ssl_socket.rb | 9 +- lib/httpclient/util.rb | 13 ++ sample/socks.rb | 16 ++ test/helper.rb | 24 +++ test/sockssvr.rb | 356 +++++++++++++++++++++++++++++++++++ test/test_httpclient.rb | 87 ++++++++- 10 files changed, 767 insertions(+), 9 deletions(-) create mode 100644 lib/httpclient/socks.rb create mode 100644 sample/socks.rb create mode 100644 test/sockssvr.rb diff --git a/README.md b/README.md index 02962645..e99bf52b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ See [HTTPClient](http://www.rubydoc.info/gems/httpclient/frames) for documentati ## Features * methods like GET/HEAD/POST/* via HTTP/1.1. -* HTTPS(SSL), Cookies, proxy, authentication(Digest, NTLM, Basic), etc. +* HTTPS(SSL), Cookies, (HTTP, socks[45])proxy, authentication(Digest, NTLM, Basic), etc. * asynchronous HTTP request, streaming HTTP request. * debug mode CLI. * by contrast with net/http in standard distribution; diff --git a/lib/httpclient.rb b/lib/httpclient.rb index 4f4b4297..c16fe95f 100644 --- a/lib/httpclient.rb +++ b/lib/httpclient.rb @@ -44,9 +44,12 @@ # clnt = HTTPClient.new # # 2. Accessing resources through HTTP proxy. You can use environment -# variable 'http_proxy' or 'HTTP_PROXY' instead. +# variable 'http_proxy' or 'HTTP_PROXY' or 'SOCKS_PROXY' instead. # # clnt = HTTPClient.new('http://myproxy:8080') +# clnt = HTTPClient.new('socks4://myproxy:1080') +# clnt = HTTPClient.new('socks5://myproxy:1080') +# clnt = HTTPClient.new('socks5://username:password@myproxy:1080') # # === How to retrieve web resources # @@ -503,8 +506,9 @@ def proxy=(proxy) @proxy_auth.reset_challenge else @proxy = urify(proxy) - if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or - @proxy.host == nil or @proxy.port == nil + if @proxy.scheme == nil or + (@proxy.scheme.downcase != 'http' and !socks?(@proxy)) or + @proxy.host == nil or @proxy.port == nil raise ArgumentError.new("unsupported proxy #{proxy}") end @proxy_auth.reset_challenge @@ -1069,8 +1073,10 @@ def load_environment # HTTP_* is used for HTTP header information. Unlike open-uri, we # simply ignore http_proxy in CGI env and use cgi_http_proxy instead. self.proxy = getenv('cgi_http_proxy') - else + elsif ENV.key?('http_proxy') self.proxy = getenv('http_proxy') + else + self.proxy = getenv('socks_proxy') end # no_proxy self.no_proxy = getenv('no_proxy') diff --git a/lib/httpclient/session.rb b/lib/httpclient/session.rb index 67e2c3ba..35259c96 100644 --- a/lib/httpclient/session.rb +++ b/lib/httpclient/session.rb @@ -21,6 +21,7 @@ require 'httpclient/timeout' # TODO: remove this once we drop 1.8 support require 'httpclient/ssl_config' require 'httpclient/http' +require 'httpclient/socks' if defined? JRUBY_VERSION require 'httpclient/jruby_ssl_socket' else @@ -92,6 +93,8 @@ def inspect # :nodoc: # Manages sessions for a HTTPClient instance. class SessionManager + include Util + # Name of this client. Used for 'User-Agent' header in HTTP request. attr_accessor :agent_name # Owner of this client. Used for 'From' header in HTTP request. @@ -132,6 +135,8 @@ class SessionManager def initialize(client) @client = client @proxy = client.proxy + @socks_user = nil + @socks_password = nil @agent_name = nil @from = nil @@ -167,6 +172,10 @@ def proxy=(proxy) @proxy = nil else @proxy = Site.new(proxy) + if socks?(proxy) + @socks_user = proxy.user + @socks_password = proxy.password + end end end @@ -218,6 +227,8 @@ def open(uri, via_proxy = false) site = Site.new(uri) sess = Session.new(@client, site, @agent_name, @from) sess.proxy = via_proxy ? @proxy : nil + sess.socks_user = @socks_user + sess.socks_password = @socks_password sess.socket_sync = @socket_sync sess.tcp_keepalive = @tcp_keepalive sess.requested_version = @protocol_version if @protocol_version @@ -448,6 +459,9 @@ class Session # Device for dumping log for debugging attr_accessor :debug_dev + attr_accessor :socks_user + attr_accessor :socks_password + attr_accessor :connect_timeout attr_accessor :connect_retry attr_accessor :send_timeout @@ -469,6 +483,8 @@ def initialize(client, dest, agent_name, from) @client = client @dest = dest @proxy = nil + @socks_user = nil + @socks_password = nil @socket_sync = true @tcp_keepalive = false @requested_version = nil @@ -627,6 +643,30 @@ def create_socket(host, port) socket end + def via_socks_proxy? + @proxy && socks?(@proxy) + end + + def via_http_proxy? + @proxy && !socks?(@proxy) + end + + def create_socks_socket(proxy, dest) + if socks4?(proxy) + options = {} + options = { user: @socks_user } if @socks_user + socks_socket = SOCKS4Socket.new(proxy.host, proxy.port, options) + elsif socks5?(proxy) + options = {} + options = { user: @socks_user } if @socks_user + options[:password] = @socks_password if @socks_password + socks_socket = SOCKS5Socket.new(proxy.host, proxy.port, options) + end + dest_site = Site.new(dest) + opened_socket = socks_socket.open(dest_site.host, dest_site.port, {}) + opened_socket + end + def create_loopback_socket(host, port, str) @debug_dev << "! CONNECT TO #{host}:#{port}\n" if @debug_dev socket = LoopBackSocket.new(host, port, str) @@ -751,6 +791,8 @@ def connect elsif https?(@dest) @socket = SSLSocket.create_socket(self) @ssl_peer_cert = @socket.peer_cert + elsif socks?(@proxy) + @socket = create_socks_socket(@proxy, @dest) else @socket = create_socket(site.host, site.port) end diff --git a/lib/httpclient/socks.rb b/lib/httpclient/socks.rb new file mode 100644 index 00000000..a2ad47fe --- /dev/null +++ b/lib/httpclient/socks.rb @@ -0,0 +1,213 @@ +# Copyright (c) 2008 Jamis Buck +# +# 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. + +require 'socket' +require 'resolv' +require 'ipaddr' + +# +# This implementation was borrowed from Net::SSH::Proxy +# +class HTTPClient + # An Standard SOCKS error. + class SOCKSError < SocketError; end + + # Used for reporting proxy connection errors. + class SOCKSConnectError < SOCKSError; end + + # Used when the server doesn't recognize the user's credentials. + class SOCKSUnauthorizedError < SOCKSError; end + + # An implementation of a SOCKS4 proxy. + class SOCKS4Socket + # The SOCKS protocol version used by this class + VERSION = 4 + + # The packet type for connection requests + CONNECT = 1 + + # The status code for a successful connection + GRANTED = 90 + + # The proxy's host name or IP address, as given to the constructor. + attr_reader :proxy_host + + # The proxy's port number. + attr_reader :proxy_port + + # The additional options that were given to the proxy's constructor. + attr_reader :options + + # Create a new proxy connection to the given proxy host and port. + # Optionally, a :user key may be given to identify the username + # with which to authenticate. + def initialize(proxy_host, proxy_port = 1080, options = {}) + @proxy_host = proxy_host + @proxy_port = proxy_port + @options = options + end + + # Return a new socket connected to the given host and port via the + # proxy that was requested when the socket factory was instantiated. + def open(host, port, connection_options) + socket = Socket.tcp(proxy_host, proxy_port, nil, nil, + connect_timeout: connection_options[:timeout]) + ip_addr = IPAddr.new(Resolv.getaddress(host)) + + packet = [ + VERSION, CONNECT, port.to_i, + ip_addr.to_i, options[:user] + ].pack('CCnNZ*') + socket.send packet, 0 + + _version, status, _port, _ip = socket.recv(8).unpack('CCnN') + if status != GRANTED + socket.close + raise SOCKSConnectError, "error connecting to socks proxy (#{status})" + end + + socket + end + end + + # An implementation of a SOCKS5 proxy. + class SOCKS5Socket + # The SOCKS protocol version used by this class + VERSION = 5 + + # The SOCKS authentication type for requests without authentication + METHOD_NO_AUTH = 0 + + # The SOCKS authentication type for requests via username/password + METHOD_PASSWD = 2 + + # The SOCKS authentication type for when there are no supported + # authentication methods. + METHOD_NONE = 0xFF + + # The SOCKS packet type for requesting a proxy connection. + CMD_CONNECT = 1 + + # The SOCKS address type for connections via IP address. + ATYP_IPV4 = 1 + + # The SOCKS address type for connections via domain name. + ATYP_DOMAIN = 3 + + # The SOCKS response code for a successful operation. + SUCCESS = 0 + + # The proxy's host name or IP address + attr_reader :proxy_host + + # The proxy's port number + attr_reader :proxy_port + + # The map of options given at initialization + attr_reader :options + + # Create a new proxy connection to the given proxy host and port. + # Optionally, :user and :password options may be given to + # identify the username and password with which to authenticate. + def initialize(proxy_host, proxy_port = 1080, options = {}) + @proxy_host = proxy_host + @proxy_port = proxy_port + @options = options + end + + # Return a new socket connected to the given host and port via the + # proxy that was requested when the socket factory was instantiated. + def open(host, port, connection_options) + socket = Socket.tcp(proxy_host, proxy_port, nil, nil, + connect_timeout: connection_options[:timeout]) + + methods = [METHOD_NO_AUTH] + methods << METHOD_PASSWD if options[:user] + + packet = [VERSION, methods.size, *methods].pack('C*') + socket.send packet, 0 + + version, method = socket.recv(2).unpack('CC') + if version != VERSION + socket.close + raise SOCKSError, "invalid SOCKS version (#{version})" + end + + if method == METHOD_NONE + socket.close + raise SOCKSError, 'no supported authorization methods' + end + + negotiate_password(socket) if method == METHOD_PASSWD + + packet = [VERSION, CMD_CONNECT, 0].pack('C*') + + if host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ + packet << [ATYP_IPV4, $1.to_i, $2.to_i, $3.to_i, $4.to_i].pack('C*') + else + packet << [ATYP_DOMAIN, host.length, host].pack('CCA*') + end + + packet << [port].pack('n') + socket.send packet, 0 + + _version, reply, = socket.recv(2).unpack('C*') + socket.recv(1) + address_type = socket.recv(1).getbyte(0) + case address_type + when 1 + socket.recv(4) # get four bytes for IPv4 address + when 3 + len = socket.recv(1).getbyte(0) + _hostname = socket.recv(len) + when 4 + _ipv6addr _hostname = socket.recv(16) + else + socket.close + raise SOCKSConnectError, 'Illegal response type' + end + _portnum = socket.recv(2) + + unless reply == SUCCESS + socket.close + raise SOCKSConnectError, reply.to_s + end + + socket + end + + private + + # Simple username/password negotiation with the SOCKS5 server. + def negotiate_password(socket) + packet = [ + 0x01, options[:user].length, options[:user], + options[:password].length, options[:password] + ].pack('CCA*CA*') + socket.send packet, 0 + + _version, status = socket.recv(2).unpack('CC') + + return if status == SUCCESS + socket.close + raise SOCKSUnauthorizedError, 'could not authorize user' + end + end +end diff --git a/lib/httpclient/ssl_socket.rb b/lib/httpclient/ssl_socket.rb index 14801428..ffa7e4c9 100644 --- a/lib/httpclient/ssl_socket.rb +++ b/lib/httpclient/ssl_socket.rb @@ -18,9 +18,14 @@ def self.create_socket(session) :debug_dev => session.debug_dev } site = session.proxy || session.dest - socket = session.create_socket(site.host, site.port) + if session.via_socks_proxy? + socket = session.create_socks_socket(session.proxy, session.dest) + else + socket = session.create_socket(site.host, site.port) + end + begin - if session.proxy + if session.via_http_proxy? session.connect_ssl_proxy(socket, Util.urify(session.dest.to_s)) end new(socket, session.dest, session.ssl_config, opts) diff --git a/lib/httpclient/util.rb b/lib/httpclient/util.rb index 6ff38016..64d767cd 100644 --- a/lib/httpclient/util.rb +++ b/lib/httpclient/util.rb @@ -216,6 +216,19 @@ def https?(uri) def http?(uri) uri.scheme && uri.scheme.downcase == 'http' end + + def socks?(uri) + uri && (socks4?(uri) || socks5?(uri)) + end + + def socks4?(uri) + uri && uri.scheme && uri.scheme.downcase == 'socks4' + end + + def socks5?(uri) + uri && uri.scheme && uri.scheme.downcase == 'socks5' + end + end diff --git a/sample/socks.rb b/sample/socks.rb new file mode 100644 index 00000000..cb2ccf60 --- /dev/null +++ b/sample/socks.rb @@ -0,0 +1,16 @@ +require 'httpclient' + +# ssh -fN -D 9999 remote_server +clnt = HTTPClient.new('socks4://localhost:9999') + +#clnt = HTTPClient.new('socks5://localhost:9999') +#clnt = HTTPClient.new('socks5://username:password@localhost:9999') + +#socks_proxy = ENV['SOCKS_PROXY'] +#clnt = HTTPClient.new(socks_proxy) + +target = 'http://www.example.com/' +puts clnt.get(target).content + +target = 'https://www.google.co.jp/' +puts clnt.get(target).content diff --git a/test/helper.rb b/test/helper.rb index 26bc4f9b..501e55a0 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -10,12 +10,14 @@ require 'httpclient' require 'webrick' +require 'webrick/https' require 'webrick/httpproxy.rb' require 'logger' require 'stringio' require 'cgi' require 'webrick/httputils' +require File.expand_path('sockssvr', File.dirname(__FILE__)) module Helper Port = 17171 @@ -37,6 +39,22 @@ def proxyurl "http://localhost:#{proxyport}/" end + def socks4_proxyurl + "socks4://username@localhost:#{proxyport}/" + end + + def socks5_noauth_proxyurl + "socks5://localhost:#{proxyport}/" + end + + def socks5_auth_proxyurl + "socks5://admin:admin@localhost:#{proxyport}/" + end + + def socks5_invalid_user_proxyurl + "socks5://invaliduser:pass@localhost:#{proxyport}/" + end + def setup @logger = Logger.new(STDERR) @logger.level = Logger::Severity::FATAL @@ -92,6 +110,12 @@ def teardown_proxyserver #@proxyserver_thread.kill end + def setup_socksproxyserver + @proxyserver = SOCKSServer.new(0) + @proxyport = @proxyserver.port + @proxyserver.start + end + def start_server_thread(server) t = Thread.new { Thread.current.abort_on_exception = true diff --git a/test/sockssvr.rb b/test/sockssvr.rb new file mode 100644 index 00000000..a146a7b7 --- /dev/null +++ b/test/sockssvr.rb @@ -0,0 +1,356 @@ +require 'logger' +require 'socket' + +TEST_USERNAME = 'admin'.freeze +TEST_PASSWORD = 'admin'.freeze + +# +# Generic SOCKS[45] server implementation +# +class SOCKSServer + attr_reader :port + + # protocol version + SOCKS_VERSION_4 = 4 + SOCKS_VERSION_5 = 5 + + # auth method + SOCKS_NO_AUTH = 0 + SOCKS_USER_PASS_AUTH = 2 + + # host type + SOCKS_HOSTTYPE_IPV4 = 1 + SOCKS_HOSTTYPE_DOMAIN = 3 + + # command + SOCKS_COMMAND_CONNECT = 1 + + # misc + SOCKS4_MAX_USERNAME = 255 + + class SOCKSProtocolError < StandardError; end + + def initialize(port = 1080) + @server = TCPServer.new(port) + addr = @server.addr + addr.shift + @port = addr[0] + @logger = Logger.new('/home/mumumu/fuga.log') + @protocol_version = nil + end + + def start + @logger.info('main thread started!') + Thread.start do + loop do + begin + Thread.start(@server.accept) do |client_socket| + @logger.info("client from #{client_socket.peeraddr[2]}") + begin + handle_client(client_socket) + rescue SOCKSProtocolError => e + @logger.warn(e) + send_protocol_error_response(client_socket) + rescue RuntimeError => e1 + @logger.warn(e1) + ensure + client_socket.close + end + end + rescue RuntimeError, IOError => e2 # @server.accept error + @logger.warn(e2) unless e2.instance_of?(IOError) + break + end + end + end + end + + def shutdown + @server.close unless @server.closed? + @logger.info('Socks server closed... ') + end + + private + + def handle_client(client_socket) + socks_version = parse_protocol_version(client_socket) + @protocol_version = socks_version + + case @protocol_version + when SOCKS_VERSION_4 # not socks4a, but socks4 + @logger.debug('entering socks4 protocol mode...') + session = create_socks4_session(client_socket) + client_socket.send([0x00, 0x5a].concat([0xFF] * 6).pack('C*'), 0) + forward_packet(session, client_socket) + when SOCKS_VERSION_5 + @logger.debug('entering socks5 protocol mode...') + auth_method = get_auth_method(client_socket) + client_socket.send([SOCKS_VERSION_5, auth_method].pack('C*'), 0) + auth_if_needed(auth_method, client_socket) + + session = create_socks5_session(client_socket) + forward_packet(session, client_socket) + end + end + + def get_username(socket) + username = nil + if @protocol_version == SOCKS_VERSION_4 + username = socket.recv(SOCKS4_MAX_USERNAME).unpack('Z*').join('') + elsif @protocol_version == SOCKS_VERSION_5 + username_length = socket.recv(1).unpack('C')[0] + username = socket.recv(username_length) + end + @logger.debug("got username -> #{username}") + username + end + + def get_password(socket) + if @protocol_version == SOCKS_VERSION_4 + raise SOCKSProtocolError, 'unsupported field -> password' + end + password_length = socket.recv(1).unpack('C')[0] + password = socket.recv(password_length) + @logger.debug("got password -> #{password}") + password + end + + def parse_protocol_version(socket) + socks_version = socket.recv(1).unpack('C')[0] + if socks_version != SOCKS_VERSION_4 && socks_version != SOCKS_VERSION_5 + raise "bad socks protocol version -> #{socks_version}" + end + socks_version + end + + def get_auth_method(socket) + number_of_methods = socket.recv(1).unpack('C')[0] + if number_of_methods < 1 + raise SOCKSProtocolError, + 'auth method must be > 0' + end + + @logger.debug("select auth method from #{number_of_methods} choice") + + choice_methods = [] + number_of_methods.times do + auth_method = socket.recv(1).unpack('C')[0] + # auth_method == 0 -> NO AUTH + # auth_method == 2 -> USERNAME:PASSWORD AUTH + if [SOCKS_NO_AUTH, SOCKS_USER_PASS_AUTH].include?(auth_method) + @logger.debug("auth method get success -> #{auth_method}") + choice_methods.push(auth_method) + end + end + + raise SOCKSProtocolError, 'unsupported auth method' if choice_methods.empty? + + case choice_methods.size + when 1 + @logger.debug("selected auth method -> #{choice_methods[0]}") + return choice_methods[0] + when 2 + @logger.debug('selected auth method -> 2') + return SOCKS_USER_PASS_AUTH + end + end + + def auth_if_needed(auth_method, socket) + return if auth_method == SOCKS_NO_AUTH + + version = socket.recv(1).unpack('C')[0] + if version != 1 + raise SOCKSProtocolError, + "invalid auth version -> #{version} must be 1" + end + + username = get_username(socket) + password = get_password(socket) + + if username == TEST_USERNAME && password == TEST_PASSWORD + @logger.debug('socks5 username:password auth -> success') + socket.send([SOCKS_VERSION_5, 0x00].pack('C*'), 0) + else + @logger.debug('socks5 username:password auth -> fail') + socket.send([SOCKS_VERSION_5, 0xFF].pack('C*'), 0) + end + end + + def create_socks4_session(socket) + command = get_command(socket) + port, raw_port = get_port(socket) + host, raw_host = get_ipv4_host(socket) + _username = get_username(socket) + session = { + address_type: SOCKS_HOSTTYPE_IPV4, + host: host, + port: port, + raw_host: raw_host, + raw_port: raw_port, + command: command + } + session + end + + def create_socks5_session(socket) + validate_protocol_version(socket) + + command = get_command(socket) + check_reserved_byte(socket) + session = get_host_and_port(socket) + session[:command] = command + session + end + + def validate_protocol_version(socket) + version = parse_protocol_version(socket) + unless @protocol_version == version + raise SOCKSProtocolError, + "protoversion mismatch #{@protocol_version} != #{version}" + end + version + end + + def get_command(socket) + @logger.debug('get connection type') + command = socket.recv(1).unpack('C')[0] + @logger.debug("command => #{command}") + command + end + + def check_reserved_byte(socket) + # reserved byte + reserved_byte = socket.recv(1).unpack('C')[0] + unless reserved_byte.zero? + raise SOCKSProtocolError, + "reserved byte must be zero, but #{reserved_byte}" + end + @logger.debug("reserved byte => #{reserved_byte}") + end + + def get_ipv4_host(socket) + raw_host = socket.recv(4).unpack('C*') + host = raw_host.join('.') + @logger.debug("host => #{host}") + if host.empty? + raise SOCKSProtocolError, + 'specified host is invalid!' + end + [host, raw_host] + end + + def get_domain(socket) + domain_length = socket.recv(1).unpack('C')[0] + if domain_length < 1 + raise SOCKSProtocolError, + 'domain length is too short!' + end + + host = socket.recv(domain_length) + raw_host = host.unpack('C*') + [host, raw_host] + end + + def get_port(socket) + raw_port = socket.recv(2).unpack('C*') + port = raw_port[0] << 8 | raw_port[1] + [port, raw_port] + end + + def get_host_and_port(socket) + address_type = socket.recv(1).unpack('C')[0] + @logger.debug("address_type #{address_type}") + + case address_type + when SOCKS_HOSTTYPE_IPV4 + @logger.debug('hostname type is IPv4') + host, raw_host = get_ipv4_host(socket) + port, raw_port = get_port(socket) + when SOCKS_HOSTTYPE_DOMAIN + @logger.debug('host type is domain name') + host, raw_host = get_domain(socket) + port, raw_port = get_port(socket) + end + + @logger.debug("host => #{host}") + @logger.debug("raw host => #{raw_host}") + @logger.debug("port => #{port}") + @logger.debug("raw port => #{raw_port}") + + session = { + address_type: address_type, + host: host, + port: port, + raw_host: raw_host, + raw_port: raw_port + } + session + end + + def forward_packet(session, client_socket) + command = session[:command] + raise 'CONNECT is only implemented' unless command == SOCKS_COMMAND_CONNECT + + client_addr = client_socket.peeraddr[2] + + host = session[:host] + port = session[:port] + + @logger.debug("client #{client_addr} connecting to #{host}:#{port} ...") + + dest_socket = TCPSocket.open(host, port) + if dest_socket + target_addr = dest_socket.peeraddr[2] + send_socks5_response(client_socket, 0, session) # success + + loop do # main io loop + readables, _, exceptions = IO.select([client_socket, dest_socket]) + raise exceptions[0] unless exceptions.empty? + + readables.each do |io| + if io == client_socket + client_msg = client_socket.recvmsg[0] + next if client_msg.length.zero? + @logger.debug("recvmsg from client => #{client_msg.length} bytes") + @logger.debug("send to dest #{target_addr} => #{client_msg}") + dest_socket.sendmsg(client_msg) + elsif io == dest_socket + dest_msg = dest_socket.recvmsg[0] + next if dest_msg.length.zero? + @logger.debug("recvmsg from dest => #{dest_msg.length} bytes") + @logger.debug("send to client #{client_addr} => #{dest_msg}") + client_socket.sendmsg(dest_msg) + end + end + end + else + @logger.error("connecting to #{host}:#{port} fail") + send_socks5_response(client_socket, 1, session) # error + end + end + + def send_protocol_error_response(client_socket) + if @protocol_version == SOCKS_VERSION_4 + client_socket.send([0x00, 0x5b].concat([0xff] * 6).pack('C*'), 0) + elsif @protocol_version == SOCKS_VERSION_5 + client_socket.send([SOCKS_VERSION_5, 0xff].pack('C*'), 0) + end + end + + def send_socks5_response(socket, code, session) + return unless @protocol_version == SOCKS_VERSION_5 + + host = session[:host] + address_type = session[:address_type] + raw_host = session[:raw_host] + raw_port = session[:raw_port] + + resp = [ + SOCKS_VERSION_5, code, 0x00 + ].push(address_type).push( + host.length + ).concat(raw_host).concat(raw_port) + @logger.debug("resp => #{resp.inspect}") + socket.send(resp.pack('C*'), 0) + end +end diff --git a/test/test_httpclient.rb b/test/test_httpclient.rb index c8e5330c..7b525f9c 100644 --- a/test/test_httpclient.rb +++ b/test/test_httpclient.rb @@ -2,7 +2,6 @@ require File.expand_path('helper', File.dirname(__FILE__)) require 'tempfile' - class TestHTTPClient < Test::Unit::TestCase include Helper include HTTPClient::Util @@ -28,6 +27,32 @@ def test_initialize end end + def test_socks_initialize + setup_socksproxyserver + + [ + socks4_proxyurl, + socks5_noauth_proxyurl, + socks5_auth_proxyurl + ].each do |proxyurl| + escape_noproxy do + @client = HTTPClient.new(proxyurl) + assert_equal(urify(proxyurl), @client.proxy) + assert_equal(200, @client.head(serverurl).status) + end + end + end + + def test_socks5_autherror + setup_socksproxyserver + escape_noproxy do + @client = HTTPClient.new(socks5_invalid_user_proxyurl) + assert_raises(HTTPClient::SOCKSUnauthorizedError) do + @client.get(serverurl) + end + end + end + def test_agent_name @client = HTTPClient.new(nil, "agent_name_foo") str = "" @@ -241,7 +266,9 @@ def test_host_header def test_proxy_env setup_proxyserver escape_env do + # put HTTP_PROXY env ahead of SOCKS_PROXY ENV['http_proxy'] = "http://admin:admin@foo:1234" + ENV['socks_proxy'] = 'socks5://admin:admin@foo:1234' ENV['NO_PROXY'] = "foobar" client = HTTPClient.new assert_equal(urify("http://admin:admin@foo:1234"), client.proxy) @@ -249,6 +276,17 @@ def test_proxy_env end end + def test_socks_proxy_env + setup_socksproxyserver + escape_env do + ENV['socks_proxy'] = 'socks5://admin:admin@foo:1234' + ENV['NO_PROXY'] = "foobar" + client = HTTPClient.new + assert_equal(urify("socks5://admin:admin@foo:1234"), client.proxy) + assert_equal('foobar', client.no_proxy) + end + end + def test_proxy_env_cgi setup_proxyserver escape_env do @@ -378,7 +416,6 @@ def test_cookie_update_while_authentication end end - def test_proxy_ssl escape_noproxy do @client.proxy = 'http://admin:admin@localhost:8080/' @@ -405,6 +442,20 @@ def test_proxy_ssl end end + def test_socks_proxy_ssl + setup_socksproxyserver + setup_sslserver + escape_noproxy do + curdir = File.dirname(File.expand_path(__FILE__)) + @client.proxy = socks5_auth_proxyurl + @client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE + assert_equal( + 'hello', + @client.get("https://localhost:#{@serverport}/hello").content + ) + end + end + def test_loopback_response @client.test_loopback_response << 'message body 1' @client.test_loopback_response << 'message body 2' @@ -1957,6 +2008,38 @@ def setup_server @server_thread = start_server_thread(@server) end + def setup_sslserver + curdir = File.dirname(File.expand_path(__FILE__)) + servercert = OpenSSL::X509::Certificate.new(File.open(File.join(curdir, 'server.cert')) { |f| + f.read + }) + key = OpenSSL::PKey::RSA.new(File.open(File.join(curdir, 'server.key')) { |f| + f.read + }) + + @server = WEBrick::HTTPServer.new( + :BindAddress => "localhost", + :Logger => @logger, + :Port => 0, + :DocumentRoot => curdir, + :SSLEnable => true, + :SSLCACertificateFile => File.join(curdir, 'ca.cert'), + :SSLCertificate => servercert, + :SSLPrivateKey => key, + :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE, + :SSLCertName => nil + ) + @serverport = @server.config[:Port] + + [:hello].each do |sym| + @server.mount( + "/#{sym}", + WEBrick::HTTPServlet::ProcHandler.new(method("do_#{sym}").to_proc) + ) + end + @server_thread = start_server_thread(@server) + end + def add_query_string(req) if req.query_string '?' + req.query_string From a56879e3e0126154ace45a27773709905e175739 Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Fri, 29 Sep 2017 13:20:07 +0900 Subject: [PATCH 02/10] - fixed test server env dependent logger --- test/sockssvr.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/sockssvr.rb b/test/sockssvr.rb index a146a7b7..ec1a8a5c 100644 --- a/test/sockssvr.rb +++ b/test/sockssvr.rb @@ -35,7 +35,8 @@ def initialize(port = 1080) addr = @server.addr addr.shift @port = addr[0] - @logger = Logger.new('/home/mumumu/fuga.log') + @logger = Logger.new(STDERR) + @logger.level = Logger::INFO @protocol_version = nil end From e7cb964ec71cbd950b3bbd2ad67f5d49633869b5 Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Mon, 2 Oct 2017 03:36:16 +0900 Subject: [PATCH 03/10] - fixed bug socks server emits 'Bad Protocol version' error when auth fails. --- test/sockssvr.rb | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/test/sockssvr.rb b/test/sockssvr.rb index ec1a8a5c..145fde16 100644 --- a/test/sockssvr.rb +++ b/test/sockssvr.rb @@ -18,6 +18,10 @@ class SOCKSServer SOCKS_NO_AUTH = 0 SOCKS_USER_PASS_AUTH = 2 + # auth result + SOCKS_AUTH_SUCCESS = 1 + SOCKS_AUTH_FAILURE = 2 + # host type SOCKS_HOSTTYPE_IPV4 = 1 SOCKS_HOSTTYPE_DOMAIN = 3 @@ -87,7 +91,11 @@ def handle_client(client_socket) @logger.debug('entering socks5 protocol mode...') auth_method = get_auth_method(client_socket) client_socket.send([SOCKS_VERSION_5, auth_method].pack('C*'), 0) - auth_if_needed(auth_method, client_socket) + unless auth_method == SOCKS_NO_AUTH + auth_result = authenticate(client_socket) + send_auth_response(auth_result, client_socket) + return unless auth_result == SOCKS_AUTH_SUCCESS + end session = create_socks5_session(client_socket) forward_packet(session, client_socket) @@ -156,9 +164,7 @@ def get_auth_method(socket) end end - def auth_if_needed(auth_method, socket) - return if auth_method == SOCKS_NO_AUTH - + def authenticate(socket) version = socket.recv(1).unpack('C')[0] if version != 1 raise SOCKSProtocolError, @@ -170,9 +176,18 @@ def auth_if_needed(auth_method, socket) if username == TEST_USERNAME && password == TEST_PASSWORD @logger.debug('socks5 username:password auth -> success') - socket.send([SOCKS_VERSION_5, 0x00].pack('C*'), 0) + return SOCKS_AUTH_SUCCESS else @logger.debug('socks5 username:password auth -> fail') + return SOCKS_AUTH_FAILURE + end + end + + def send_auth_response(auth_result, socket) + case auth_result + when SOCKS_AUTH_SUCCESS + socket.send([SOCKS_VERSION_5, 0x00].pack('C*'), 0) + when SOCKS_AUTH_FAILURE socket.send([SOCKS_VERSION_5, 0xFF].pack('C*'), 0) end end From 43847ba0258f4f0563b38fd65a27a0ad97c4cb69 Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Mon, 2 Oct 2017 03:38:25 +0900 Subject: [PATCH 04/10] - changed socks server default loglevel: INFO -> WARN --- test/sockssvr.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sockssvr.rb b/test/sockssvr.rb index 145fde16..9cb5c764 100644 --- a/test/sockssvr.rb +++ b/test/sockssvr.rb @@ -40,7 +40,7 @@ def initialize(port = 1080) addr.shift @port = addr[0] @logger = Logger.new(STDERR) - @logger.level = Logger::INFO + @logger.level = Logger::WARN @protocol_version = nil end From b24b9b69ab90c16aa8d86d5cefc6608d932f791e Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Mon, 2 Oct 2017 03:39:12 +0900 Subject: [PATCH 05/10] - raise RuntimeError in case of invalid proxy url --- lib/httpclient/session.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/httpclient/session.rb b/lib/httpclient/session.rb index 35259c96..40833849 100644 --- a/lib/httpclient/session.rb +++ b/lib/httpclient/session.rb @@ -661,6 +661,8 @@ def create_socks_socket(proxy, dest) options = { user: @socks_user } if @socks_user options[:password] = @socks_password if @socks_password socks_socket = SOCKS5Socket.new(proxy.host, proxy.port, options) + else + raise "invalid proxy url #{proxy}" end dest_site = Site.new(dest) opened_socket = socks_socket.open(dest_site.host, dest_site.port, {}) From 22f1fb8717e5743e25debe21be965375c8eb0ab1 Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Mon, 2 Oct 2017 03:39:37 +0900 Subject: [PATCH 06/10] - updated README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e99bf52b..415f2deb 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ See [HTTPClient](http://www.rubydoc.info/gems/httpclient/frames) for documentati * extensible with filter interface * you don't have to care HTTP/1.1 persistent connection (httpclient cares instead of you) + * socks proxy * Not supported now * Cache * Rather advanced HTTP/1.1 usage such as Range, deflate, etc. From c709305d4ad6a569fe012dcdda7f0ac1b078f98b Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Mon, 2 Oct 2017 03:48:29 +0900 Subject: [PATCH 07/10] - fixed ENV using sample --- sample/socks.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample/socks.rb b/sample/socks.rb index cb2ccf60..5ebbd467 100644 --- a/sample/socks.rb +++ b/sample/socks.rb @@ -6,8 +6,8 @@ #clnt = HTTPClient.new('socks5://localhost:9999') #clnt = HTTPClient.new('socks5://username:password@localhost:9999') -#socks_proxy = ENV['SOCKS_PROXY'] -#clnt = HTTPClient.new(socks_proxy) +#ENV['SOCKS_PROXY'] = 'socks5://localhost:9999' +#clnt = HTTPClient.new target = 'http://www.example.com/' puts clnt.get(target).content From e8486e1cdf07922322f75410ab3ffca0d0a0f176 Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Mon, 17 Sep 2018 18:25:24 +0900 Subject: [PATCH 08/10] applied #386 for fixing CI error. --- lib/httpclient/ssl_config.rb | 19 ++++++-- test/helper.rb | 3 +- test/test_ssl.rb | 89 +++++++++++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/lib/httpclient/ssl_config.rb b/lib/httpclient/ssl_config.rb index f6e7ce92..b8a98502 100644 --- a/lib/httpclient/ssl_config.rb +++ b/lib/httpclient/ssl_config.rb @@ -146,6 +146,9 @@ def initialize(client) return unless SSLEnabled @client = client @cert_store = X509::Store.new + @cert_store.set_default_paths + @cacerts_loaded = working_openssl_platform? + @cert_store_crl_items = [] @client_cert = @client_key = @client_key_pass = @client_ca = nil @verify_mode = SSL::VERIFY_PEER | SSL::VERIFY_FAIL_IF_NO_PEER_CERT @@ -162,7 +165,6 @@ def initialize(client) @options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3) # OpenSSL 0.9.8 default: "ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH" @ciphers = CIPHERS_DEFAULT - @cacerts_loaded = false end # Sets certificate and private key for SSL client authentication. @@ -413,10 +415,21 @@ def change_notify nil end + def working_openssl_platform? + File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) && Dir.exist?(OpenSSL::X509::DEFAULT_CERT_DIR) + end + # Use 2048 bit certs trust anchor def load_cacerts(cert_store) - file = File.join(File.dirname(__FILE__), 'cacert.pem') - add_trust_ca_to_store(cert_store, file) + certs = if ENV.key?('SSL_CERT_DIR'.freeze) || ENV.key?('SSL_CERT_FILE') + [ ENV['SSL_CERT_DIR'], ENV['SSL_CERT_FILE'] ].compact + else + [ File.join(File.dirname(__FILE__), 'cacert.pem') ] + end + + certs.each do |cert| + add_trust_ca_to_store(cert_store, cert) + end end end diff --git a/test/helper.rb b/test/helper.rb index 501e55a0..881303dc 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -5,7 +5,8 @@ SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter SimpleCov.start rescue LoadError -end +end if ENV['CI'] + require 'test/unit' require 'httpclient' diff --git a/test/test_ssl.rb b/test/test_ssl.rb index 2e634d71..0e1a9b73 100644 --- a/test/test_ssl.rb +++ b/test/test_ssl.rb @@ -81,10 +81,42 @@ def test_debug_dev end def test_verification_without_httpclient - raw_cert = "-----BEGIN CERTIFICATE-----\nMIIDOTCCAiGgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBCMRMwEQYKCZImiZPyLGQB\nGRYDb3JnMRkwFwYKCZImiZPyLGQBGRYJcnVieS1sYW5nMRAwDgYDVQQDDAdSdWJ5\nIENBMB4XDTE2MDgxMDE3MjEzNFoXDTE3MDgxMDE3MjEzNFowSzETMBEGCgmSJomT\n8ixkARkWA29yZzEZMBcGCgmSJomT8ixkARkWCXJ1YnktbGFuZzEZMBcGA1UEAwwQ\nUnVieSBjZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAJCfsSXpSMpmZCVa+ZCM+QDgomnhDlvnrGDq6pasTaIspGTXgws+7r8Dt/cNe6EH\nHJpRH2cGRiO4yPcfcT9eS4X7k8OC4f33wHfACOmLu6LeoNE8ujmSk6L6WzLUI+sE\nnLZbFrXxoAo4XHsm8vEG9C+jEoXZ1p+47wrAGaDwDQTnzlMy4dT9pRQEJP2G/Rry\nUkuZn8SUWmh3/YS78iaSzsNF1cgE1ealHOrPPFDjiCGDaH/LHyUPYlbFSLZ/B7Qx\nLxi5sePLcywWq/EJrmWpgeVTDjtNijsdKv/A3qkY+fm/oD0pzt7XsfJaP9YKNyJO\nQFdxWZeiPcDF+Hwf+IwSr+kCAwEAAaMxMC8wDgYDVR0PAQH/BAQDAgeAMB0GA1Ud\nDgQWBBQNvzYzJyXemGhxbA8NMXLolDnPyjANBgkqhkiG9w0BAQsFAAOCAQEARIJV\noKejGlOTn71QutnNnu07UtTu0IHs6YqjYzzND+m4JXLN+wvYm72AFUG0b1L7dRg0\niK8XjQrlNQNVqP1Mc6tffchy20neOPOHeiO6qTdRU8P2S8D3Uwe+1qhgxjfE+cWc\nwZmWxYK4HA8c58PxWMqrkr2QqXDplG9KWLvOgrtPGiLLZcQSKhvvB63QzItHBDU6\nRayiJY3oPkK/HrIvFlySqFqzWmuyknkciOFywEHQMz/tcSFJ2QFpPj/tBz9VXohH\nZ8KscmfhZrTPBjo+ky1lz/WraWoz4LMiLnkC2ABczWLRSawu+v3Irx1NFJngt05e\npqwtqIUeg7j+JLiTaA==\n-----END CERTIFICATE-----" - raw_ca_cert = "-----BEGIN CERTIFICATE-----\nMIIDYjCCAkqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBCMRMwEQYKCZImiZPyLGQB\nGRYDb3JnMRkwFwYKCZImiZPyLGQBGRYJcnVieS1sYW5nMRAwDgYDVQQDDAdSdWJ5\nIENBMB4XDTE2MDgxMDE3MjA1NFoXDTE4MDgxMDE3MjA1NFowQjETMBEGCgmSJomT\n8ixkARkWA29yZzEZMBcGCgmSJomT8ixkARkWCXJ1YnktbGFuZzEQMA4GA1UEAwwH\nUnVieSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKGwyM3Ejtl\npo7CqaDlS71gDZn3gm6IwWpmRMLJofSI9LCwAbjijSC2HvO0xUWoYW40FbzjnnEi\ngszsWyPwuQIx9t0bhuAyllNIfImmkaQkrikXKBKzia4jPnbc4iXPnfjuThjESFWl\ntfbN6y1B5TjKhD1KelfakUO+iMu8WlIA9NKQZYfJ/F3QSpP5Iqb3KN/jVifFbDV8\nbAl3Ln4rT2kTCKrZZcl1jmWsJv8jBw6+P7hk0/Mu0JeHAITsjbNbpHd8UXpCfbVs\nsNGZrBU4uJdZ2YTG+Y27/t25jFNQwb+TWbvig7rfdX2sjssuxa00BBxarC08tIVj\nZprM37KcNn8CAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\nAQYwHQYDVR0OBBYEFA2/NjMnJd6YaHFsDw0xcuiUOc/KMB8GA1UdIwQYMBYEFA2/\nNjMnJd6YaHFsDw0xcuiUOc/KMA0GCSqGSIb3DQEBCwUAA4IBAQAJSOw49XqvUll0\n3vU9EAO6yUdeZSsQENIfYbRMQgapbnN1vTyrUjPZkGC5hIE1pVdoHtEoUEICxIwy\nr6BKxiSLBDLp+rvIuDdzMkXIWdUVvTZguVRyKtM2gfnpsPLpVnv+stBmAW2SMyxm\nkymhOpkjdv3He+45uorB3tdfBS9VVomDEUJdg38UE1b5eXRQ3D6gG0iCPFzKszXg\nLoAYhGxtjCJaKlbzduMK0YO6aelgW1+XnVIKcA7DJ9egk5d/dFZBPFfwumwr9hTH\nh7/fp3Fr87weI+CkfmFyJZrsEBlXJBVuvPesMVHTh3Whm5kmCdWcBJU0QmSq42ZL\n72U0PXLR\n-----END CERTIFICATE-----" - ca_cert = ::OpenSSL::X509::Certificate.new(raw_ca_cert) - cert = ::OpenSSL::X509::Certificate.new(raw_cert) + ca_cert = ::OpenSSL::X509::Certificate.new(%w[-----BEGIN\ CERTIFICATE----- +MIIC3jCCAcYCCQCUWi3t8e122TANBgkqhkiG9w0BAQsFADAxMQ0wCwYDVQQKDARS +dWJ5MRMwEQYDVQQLDApodHRwY2xpZW50MQswCQYDVQQDDAJDQTAeFw0xODAyMjcx +MTM0NDRaFw0yODAyMjUxMTM0NDRaMDExDTALBgNVBAoMBFJ1YnkxEzARBgNVBAsM +Cmh0dHBjbGllbnQxCzAJBgNVBAMMAkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAs6FPPj8PVl1uxsMZas4VC/ibRvtyXQkfrEa7TO032Kh+ETsOQNS8 +QJedhw/BMHuoVbU0/b6PZ//LJTUDN/C77/QWHKzcMoxkNye5PC2cJlSQMosaKjYG +1ERYmJ+FBiMMSpcLOCS5cYoP2fJHGtHqZPkxIPYy+IKQ7WuP3tUXkVC+ftpD6H4V +6MUnfLwagpaAAbRoFUJQoZISmH2+F5GOKX9KKiMBI94yqRRN4K/B9iqXgld45Hmg +67vX0ckRbqBhrz1CwPtaETLFB4hZT2ouBkMQYtrvpNXv80p7vcz+BwORo8b2Ns9B +4FqtpjMaS9Mf95z4Mn+NG7lanYtsHO2svwIDAQABMA0GCSqGSIb3DQEBCwUAA4IB +AQBu614zHB5SS+ORYrRwl7tICKUipWHdCJfYsJOQy/FKwe7vedwd/Uclfe06GU+m +bNv0y22/oF7vrM3EfnxFe2DNIKXTndszrQSLpT6OPBe4mAOSJxnIMy6B6/PyhK6I +D7TWFSVlYX9a4OfolsoE0gQtxhyLud4rvJgXyAq9kRZ1FcNfI75cImk67rCa8jRY +TJOTidKq1Kcn6RY7d8cf581HP7y/eK887K6lBvGiQE1aFDSLe2ZLY+rxS9GSMYfK +81XhUX2QKytGYch2y95ThMwOljVTg6fKDrtKGwj9mSsnlfTFX3gikvLLtB/o7JPR +2pWBic8PX7gnANQqH/4ahv1M +-----END\ CERTIFICATE-----].join("\n")) + cert = ::OpenSSL::X509::Certificate.new(%w[-----BEGIN\ CERTIFICATE----- +MIIC5zCCAc8CCQCz/lMJNLxQDjANBgkqhkiG9w0BAQsFADAxMQ0wCwYDVQQKDARS +dWJ5MRMwEQYDVQQLDApodHRwY2xpZW50MQswCQYDVQQDDAJDQTAeFw0xODAyMjcx +MTM1MTNaFw0yNzExMjcxMTM1MTNaMDoxDTALBgNVBAoMBFJ1YnkxEzARBgNVBAsM +Cmh0dHBjbGllbnQxFDASBgNVBAMMC0NlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAo/zP4oPyqerNyJYNTKzAGGQR8uKmP9wLnLm/yTf/ +jwzVLj3rvunw54aw89V3R4LLwBBMgFlE9OrUa+2zCvZJ8ykSoltU+w9E2EdXnXAR +C/GW678MA06NPBuMNQyf+7Lv7dipdv+0hUNXFarwGiJkCms0zcmTonkOC8Bh7stZ +EykkvQs5zmYVd+G26D5un8Wzjl6OckbBDcKTS9u9H1YveRcnN7odsh+qI4PjDmKG +PXR8Gz/loNYN/I55Hqe7vkQJZ7r1PjSBp/fIcb4pNEkKS9DAcNWkoHF2j5nBNdOq +mH3WR36vKlw5S4HLzDXQDeueFbtk3QGrWY2MWrpJNapeAQIDAQABMA0GCSqGSIb3 +DQEBCwUAA4IBAQB2CiGKAvHjr4kjOavWqGfPv115N4fhmBcPH4YAeJB9mHTzpoPV +BCm0ouRG5Oqj/DJhm+mckFKSorZFSgVb/G92w0uXRvBMPJb4wyIbp5ld6K3138cn +DtmeON3gbHwh3or741LdD6GIaulA9CL/qI3bbiyrJrHAZuHbpA6UqHfTKTBVi0uq +kv8qmA8FrzI2itDqdp0dq3QMNGnG40OM8NSDX+8A9wMahPh+Oe3TePSvDTahXIU1 +o+dzaUEIVhUWEikQBnfeEnxzN8B/qtt3wEpliAip9Z3LuN0pVFb81Mx1wEZls2Bd +Kj83iBw7flO651USNPnkOkU3DegNtcpTaT5M +-----END\ CERTIFICATE-----].join("\n")) store = ::OpenSSL::X509::Store.new store.add_cert(ca_cert) assert(store.verify(cert)) @@ -245,10 +277,39 @@ def test_set_default_paths end end + def test_load_cacerts + omit_if(RUBY_ENGINE == 'jruby', 'SSL_CERT_FILE environment does not work on JRuby') + + # disables loading default openssl paths + stub_x509_const(:DEFAULT_CERT_FILE, '/invalid') do + assert_raise(OpenSSL::SSL::SSLError) do + @client.get(@url) + end + + setup_client + + escape_env do + ENV['SSL_CERT_FILE'] = File.join(DIR, 'ca-chain.pem') + @client.get(@url) + end + end + end + + def test_default_paths + assert_raise(OpenSSL::SSL::SSLError) do + @client.get(@url) + end + escape_env do + ENV['SSL_CERT_FILE'] = File.join(DIR, 'ca-chain.pem') + setup_client + @client.get(@url) + end + end + def test_no_sslv3 teardown_server setup_server_with_ssl_version(:SSLv3) - assert_raise(OpenSSL::SSL::SSLError) do + assert_raise() do @client.ssl_config.verify_mode = nil @client.get("https://localhost:#{serverport}/hello") end @@ -264,7 +325,7 @@ def test_allow_tlsv1 end def test_use_higher_TLS - omit('TODO: it does not pass with Java 7 or old openssl ') + # TODO: it does not pass with Java 7 or old openssl teardown_server setup_server_with_ssl_version('TLSv1_2') assert_nothing_raised do @@ -429,6 +490,20 @@ def test_timeout private + def stub_x509_const(name, value) + OpenSSL::X509.module_eval do + begin + original = remove_const(name) + const_set(name, value) + + yield + ensure + remove_const(name) + const_set(name, original) + end + end + end + def cert(filename) OpenSSL::X509::Certificate.new(File.read(File.join(DIR, filename))) end @@ -474,7 +549,7 @@ def setup_server_with_ssl_version(ssl_version) ssl_version = ssl_version.tr('_', '.') end logger = Logger.new(STDERR) - logger.level = Logger::Severity::FATAL # avoid logging SSLError (ERROR level) + logger.level = Logger::Severity::FATAL # avoid logging SSLError (ERROR level) @server = WEBrick::HTTPServer.new( :BindAddress => "localhost", :Logger => logger, From 9aeb8e7483743a0b8c51ff87e5ea98eaac4ed3c6 Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Tue, 5 Feb 2019 05:46:12 +0900 Subject: [PATCH 09/10] followed origin/master changes --- lib/httpclient/ssl_config.rb | 19 +++---------------- test/helper.rb | 3 +-- test/test_httpclient.rb | 2 ++ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/lib/httpclient/ssl_config.rb b/lib/httpclient/ssl_config.rb index b8a98502..f6e7ce92 100644 --- a/lib/httpclient/ssl_config.rb +++ b/lib/httpclient/ssl_config.rb @@ -146,9 +146,6 @@ def initialize(client) return unless SSLEnabled @client = client @cert_store = X509::Store.new - @cert_store.set_default_paths - @cacerts_loaded = working_openssl_platform? - @cert_store_crl_items = [] @client_cert = @client_key = @client_key_pass = @client_ca = nil @verify_mode = SSL::VERIFY_PEER | SSL::VERIFY_FAIL_IF_NO_PEER_CERT @@ -165,6 +162,7 @@ def initialize(client) @options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3) # OpenSSL 0.9.8 default: "ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH" @ciphers = CIPHERS_DEFAULT + @cacerts_loaded = false end # Sets certificate and private key for SSL client authentication. @@ -415,21 +413,10 @@ def change_notify nil end - def working_openssl_platform? - File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) && Dir.exist?(OpenSSL::X509::DEFAULT_CERT_DIR) - end - # Use 2048 bit certs trust anchor def load_cacerts(cert_store) - certs = if ENV.key?('SSL_CERT_DIR'.freeze) || ENV.key?('SSL_CERT_FILE') - [ ENV['SSL_CERT_DIR'], ENV['SSL_CERT_FILE'] ].compact - else - [ File.join(File.dirname(__FILE__), 'cacert.pem') ] - end - - certs.each do |cert| - add_trust_ca_to_store(cert_store, cert) - end + file = File.join(File.dirname(__FILE__), 'cacert.pem') + add_trust_ca_to_store(cert_store, file) end end diff --git a/test/helper.rb b/test/helper.rb index 881303dc..501e55a0 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -5,8 +5,7 @@ SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter SimpleCov.start rescue LoadError -end if ENV['CI'] - +end require 'test/unit' require 'httpclient' diff --git a/test/test_httpclient.rb b/test/test_httpclient.rb index 7b525f9c..ba95f477 100644 --- a/test/test_httpclient.rb +++ b/test/test_httpclient.rb @@ -2,6 +2,7 @@ require File.expand_path('helper', File.dirname(__FILE__)) require 'tempfile' + class TestHTTPClient < Test::Unit::TestCase include Helper include HTTPClient::Util @@ -416,6 +417,7 @@ def test_cookie_update_while_authentication end end + def test_proxy_ssl escape_noproxy do @client.proxy = 'http://admin:admin@localhost:8080/' From d9e37abe332fea72fbd8a981f173c024ba7be3c7 Mon Sep 17 00:00:00 2001 From: Yoshinari Takaoka Date: Tue, 5 Feb 2019 07:03:03 +0900 Subject: [PATCH 10/10] fixed JRuby CI error --- test/sockssvr.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/sockssvr.rb b/test/sockssvr.rb index 9cb5c764..f64fabf4 100644 --- a/test/sockssvr.rb +++ b/test/sockssvr.rb @@ -58,6 +58,8 @@ def start send_protocol_error_response(client_socket) rescue RuntimeError => e1 @logger.warn(e1) + rescue Errno::ECONNRESET => e2 + @logger.warn("SOCKSServer: Connection Reset by peer") ensure client_socket.close end @@ -325,13 +327,13 @@ def forward_packet(session, client_socket) readables.each do |io| if io == client_socket - client_msg = client_socket.recvmsg[0] + client_msg = client_socket.recv(4096) next if client_msg.length.zero? @logger.debug("recvmsg from client => #{client_msg.length} bytes") @logger.debug("send to dest #{target_addr} => #{client_msg}") dest_socket.sendmsg(client_msg) elsif io == dest_socket - dest_msg = dest_socket.recvmsg[0] + dest_msg = dest_socket.recv(4096) next if dest_msg.length.zero? @logger.debug("recvmsg from dest => #{dest_msg.length} bytes") @logger.debug("send to client #{client_addr} => #{dest_msg}")