diff --git a/.travis.yml b/.travis.yml index f6cdf4f..de9c287 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,13 @@ +sudo: true language: ruby cache: bundler +addons: + apt: + packages: + - node + before_install: - - 'test Node = "$PACPROXY_RUNTIME" && sudo apt-get update || true' - - 'test Node = "$PACPROXY_RUNTIME" && sudo apt-get install node || true' - 'test Node = "$PACPROXY_RUNTIME" && (cd lib/pacproxy/runtimes/node ; npm install) || true' rvm: diff --git a/lib/pacproxy/pacproxy.rb b/lib/pacproxy/pacproxy.rb index 7a71706..e1e1bf3 100644 --- a/lib/pacproxy/pacproxy.rb +++ b/lib/pacproxy/pacproxy.rb @@ -1,14 +1,23 @@ require 'pacproxy' -require 'webrick/httpproxy' +require 'socket' require 'uri' module Pacproxy # Pacproxy::Pacproxy represent http/https proxy server - class Pacproxy < WEBrick::HTTPProxyServer # rubocop:disable ClassLength + class Pacproxy include Loggable - def initialize(config = {}, default = WEBrick::Config::HTTP) - super({ Port: config['port'], Logger: general_logger }, default) + attr_reader :status + + REQUEST_LINE_REGEXP = + /^(?\w+)\s+(?\S+)\s+HTTP\/(?\d\.\d)\s*$/ + BUFFER_SIZE = 1024 * 16 + + def initialize(config = {}) + # TODO: Logger + @status = :Stop + @host = config['host'] + @port = config['port'] @auth = config['auth'] return unless config['pac_file'] && config['pac_file']['location'] @@ -16,140 +25,132 @@ def initialize(config = {}, default = WEBrick::Config::HTTP) config['pac_file']['update_interval']) end + def start + @socks = [] + @socket = TCPServer.new(@host, @port) + @status = :Running + loop do + s = @socket.accept + Thread.new(s, &method(:handle_request)) + end + rescue Errno::EADDRINUSE => e + STDERR.puts e + sleep 3 + retry + rescue => e + STDERR.puts e + ensure + shutdown + end + def shutdown + STDERR.puts("start: pacproxy socket.closed?:#{@socket.closed?}") if @socket + if @socket + @socket.close + @socket = nil + end + @socks.each do |s| + s.close unless s.closed? + end @pac.shutdown if @pac - super + @status = :Stop + STDERR.puts("done: pacproxy socket.closed?:#{@socket.closed?}") if @socket end - def proxy_uri(req, res) - super(req, res) - return unless @pac - - proxy_line = @pac.find(request_uri(req)) - proxy = lookup_proxy_uri(proxy_line) - create_proxy_uri(proxy, req.header) - end + private - def create_proxy_uri(proxy, header) - return nil unless proxy - return URI.parse("http://#{proxy}") unless - @auth || header.key?('proxy-authorization') - - if @auth - basic_auth = "#{@auth['user']}:#{@auth['password']}" - elsif header.key?('proxy-authorization') - auth = header['proxy-authorization'][0] - pattern = /basic (\S+)/i - basic_auth = pattern.match(auth)[1].unpack('m').first - header.delete('proxy-authorization') + def handle_request(client_s) + @socks << client_s + request_line = client_s.readline + match_result = request_line.match(REQUEST_LINE_REGEXP) + method = match_result[:method] + unparsed_uri = match_result[:unparsed_uri] + version = match_result[:version] + + proxy = if method == 'CONNECT' + @pac.find("https://#{unparsed_uri}") + else + @pac.find(unparsed_uri) + end + # TODO: recover + if method == 'CONNECT' && proxy == 'DIRECT' + do_connect(client_s, method, unparsed_uri, version) + else + unparsed_uri = "https://#{unparsed_uri}" if method == 'CONNECT' + do_request(client_s, method, unparsed_uri, version, proxy) end - - URI.parse("http://#{basic_auth}@#{proxy}") end - # This method is mainly from WEBrick::HTTPProxyServer. - # To allow upstream proxy authentication, - # it operate 407 response from an upstream proxy. - # see: https://github.com/ruby/ruby/blob/trunk/lib/webrick/httpproxy.rb - # rubocop:disable all - def do_CONNECT(req, res) - # Proxy Authentication - proxy_auth(req, res) - - ua = Thread.current[:WEBrickSocket] # User-Agent - raise WEBrick::HTTPStatus::InternalServerError, - "[BUG] cannot get socket" unless ua - - host, port = req.unparsed_uri.split(":", 2) - # Proxy authentication for upstream proxy server - if proxy = proxy_uri(req, res) - proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0" - if proxy.userinfo - credentials = "Basic " + [proxy.userinfo].pack("m").delete("\n") - end - host, port = proxy.host, proxy.port - end - - begin - @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.") - os = TCPSocket.new(host, port) # origin server - - if proxy - @logger.debug("CONNECT: sending a Request-Line") - os << proxy_request_line << WEBrick::CRLF - @logger.debug("CONNECT: > #{proxy_request_line}") - if credentials - @logger.debug("CONNECT: sending a credentials") - os << "Proxy-Authorization: " << credentials << WEBrick::CRLF - end - os << WEBrick::CRLF - proxy_status_line = os.gets(WEBrick::LF) - @logger.debug("CONNECT: read a Status-Line form the upstream server") - @logger.debug("CONNECT: < #{proxy_status_line}") - if /^HTTP\/\d+\.\d+\s+(?200|407)\s*/ =~ proxy_status_line - res.status = st.to_i - while line = os.gets(WEBrick::LF) - res.header['Proxy-Authenticate'] = - line.split(':')[1] if /Proxy-Authenticate/i =~ line - break if /\A(#{WEBrick::CRLF}|#{WEBrick::LF})\z/om =~ line - end - else - raise WEBrick::HTTPStatus::BadGateway - end - end - @logger.debug("CONNECT #{host}:#{port}: succeeded") - rescue => ex - @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'") - res.set_error(ex) - raise WEBrick::HTTPStatus::EOFError - ensure - if handler = @config[:ProxyContentHandler] - handler.call(req, res) - end - res.send_response(ua) - accesslog(req, res) - - # Should clear request-line not to send the response twice. - # see: HTTPServer#run - req.parse(WEBrick::NullReader) rescue nil + def do_request(client_s, method, unparsed_uri, version, proxy) + if proxy =~ /^proxy/i + host, port = find_proxy_uri(proxy).split(':') + port ||= 80 + server_s = TCPSocket.new(host, port) + server_s.write("#{method} #{unparsed_uri} HTTP/#{version}\r\n") + write_proxy_credential(server_s) + else + uri = URI.parse(unparsed_uri) + # TODO: recover + server_s = TCPSocket.new(uri.host, uri.port) + server_s.write("#{method} #{uri.path}?#{uri.query} HTTP/#{version}\r\n") end - begin - while fds = IO::select([ua, os]) - if fds[0].member?(ua) - buf = ua.sysread(1024); - @logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent") - os.syswrite(buf) - elsif fds[0].member?(os) - buf = os.sysread(1024); - @logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}") - ua.syswrite(buf) - end + # TODO: write log + loop do + line = client_s.readline + if line =~ /^proxy/i && proxy == 'DIRECT' + # Strip proxy headers + next + elsif line.strip.empty? + server_s.write("Connection: close\r\n\r\n") + break + else + server_s.write(line) end - rescue - os.close - @logger.debug("CONNECT #{host}:#{port}: closed") end - raise WEBrick::HTTPStatus::EOFError + transfer_data(client_s, server_s) + ensure + server_s.close unless server_s.closed? + client_s.close unless client_s.closed? end - # rubocop:enable all - def proxy_auth(req, res) - @config[:ProxyAuthProc].call(req, res) if @config[:ProxyAuthProc] + def do_connect(client_s, _method, unparsed_uri, _version) + uri = URI.parse("https://#{unparsed_uri}") + # TODO: recover + server_s = TCPSocket.new(uri.host, uri.port) + client_s.read_nonblock(BUFFER_SIZE) + client_s.write("HTTP/1.0 200 Connection Established\r\n\r\n") + + # TODO: write log + transfer_data(client_s, server_s) + ensure + server_s.close unless server_s.closed? + client_s.close unless client_s.closed? end - private - - def request_uri(request) - if 'CONNECT' == request.request_method - "https://#{request.unparsed_uri}/" - else - request.unparsed_uri + def transfer_data(client_s, server_s) + while (fds = IO.select([client_s, server_s])) + if fds[0].member?(client_s) + server_s.write(client_s.read_nonblock(BUFFER_SIZE)) + elsif fds[0].member?(server_s) + client_s.write(server_s.read_nonblock(BUFFER_SIZE)) + end end + rescue => e + STDOUT.puts('Error' + e) + ensure + STDOUT.puts('server_s client_s closed') + end + + def write_proxy_credential(server_s) + return unless @auth + credentials = 'Basic ' + + ["#{@auth['user']}:#{@auth['password']}"].pack('m').delete("\n") + server_s.write('Proxy-authorization: ' + credentials + "\r\n") end - def lookup_proxy_uri(proxy_line) + def find_proxy_uri(proxy_line) case proxy_line when /^DIRECT/ nil @@ -158,39 +159,5 @@ def lookup_proxy_uri(proxy_line) /PROXY (.*)/.match(primary_proxy)[1] end end - - # This method is mainly from WEBrick::HTTPProxyServer. - # proxy-authenticate can be transferred from a upstream proxy server - # to a client - # see: https://github.com/ruby/ruby/blob/trunk/lib/webrick/httpproxy.rb - HOP_BY_HOP = %w( connection keep-alive upgrade - proxy-authorization te trailers transfer-encoding ) - SHOULD_NOT_TRANSFER = %w( set-cookie proxy-connection ) - def choose_header(src, dst) - connections = split_field(src['connection']) - src.each do |key, value| - key = key.downcase - next if HOP_BY_HOP.member?(key) || # RFC2616: 13.5.1 - connections.member?(key) || # RFC2616: 14.10 - SHOULD_NOT_TRANSFER.member?(key) # pragmatics - - dst[key] = value - end - end - - def perform_proxy_request(req, res) - super - accesslog(req, res) - end - - # allow PUT method on proxy server - # method names for webrick is indicated by rubocop - # rubocop:disable all - def do_PUT(req, res) - perform_proxy_request(req, res) do |http, path, header| - http.put(path, req.body || '', header) - end - end - # rubocop:enable all end end diff --git a/lib/pacproxy/runtimes/node/node.rb b/lib/pacproxy/runtimes/node/node.rb index eb484ec..ca294a1 100644 --- a/lib/pacproxy/runtimes/node/node.rb +++ b/lib/pacproxy/runtimes/node/node.rb @@ -63,7 +63,11 @@ def shutdown if OS.windows? stop_server(@server_pid) else - Process.kill(:INT, @server_pid) + begin + Process.kill(:INT, @server_pid) + rescue Errno::ESRCH + lwarn('No process is Running.') + end end end @@ -150,7 +154,11 @@ def start_server def stop_server(server_info) require 'win32/process' return unless server_info || server_info.respond_to?(:process_id) - Process.kill('ExitProcess', [server_info.process_id]) + begin + Process.kill('ExitProcess', [server_info.process_id]) + rescue Errno::ESRCH + lwarn('No process is Running.') + end end end end diff --git a/spec/pacproxy_spec.rb b/spec/pacproxy_spec.rb index 550e75d..7756f73 100644 --- a/spec/pacproxy_spec.rb +++ b/spec/pacproxy_spec.rb @@ -3,10 +3,14 @@ require 'webrick/https' def wait_server_status(servers, status) + STDOUT.puts(status.to_s) return unless servers || status servers = [servers] unless servers.respond_to?(:all?) return unless servers.all? { |s| s.respond_to?(:status) } - sleep(0.01) until servers.all? { |s| s.status == status } + sleep(0.01) until servers.all? do |s| + puts("#{s.class}: #{s.status}") + s.status == status + end end describe Pacproxy do @@ -18,6 +22,7 @@ def wait_server_status(servers, status) describe 'Pacproxy#proxy_uri' do before(:each) do + STDERR.puts(`netstat -an | grep \'.13\'`) $stdout, $stderr = StringIO.new, StringIO.new @http_server = WEBrick::HTTPServer.new(Port: 13_080) @http_server.define_singleton_method(:service) do |_req, res| @@ -40,15 +45,19 @@ def wait_server_status(servers, status) after(:each) do $stdout, $stderr = STDOUT, STDERR + STDERR.puts 'after called' @http_server.shutdown @https_server.shutdown @proxy_server.shutdown @pacproxy_server.shutdown + STDERR.puts 'shutdwon call done' wait_server_status([@http_server, @https_server, @proxy_server, @pacproxy_server], :Stop) + STDERR.puts 'wait_server_status done' + STDERR.puts(`netstat -an | grep \'.13\'|grep tcp4`) end it 'transfer request to server directly' do @@ -62,26 +71,40 @@ def wait_server_status(servers, status) c = HTTPClient.new('http://127.0.0.1:13128') res = c.get('http://127.0.0.1:13080/') expect(res.status).to eq(200) - res = c.get('http://127.0.0.1:13080/noproxy/') expect(res.status).to eq(200) + c.reset_all + STDERR.puts('##c.reset_all##') end it 'transfer request to server directly via HTTPS' do + STDERR.puts 'transfer request to server directly via HTTPS started' c = Pacproxy::Config.instance.config + STDERR.puts 'Pacproxy::Config.instance.config' c['port'] = 13_128 c['pac_file']['location'] = 'spec/all_direct.pac' + STDERR.puts 'Pacproxy::Pacproxy.new(c)' @pacproxy_server = Pacproxy::Pacproxy.new(c) + STDERR.puts 'Thread.new { @pacproxy_server.start } start' Thread.new { @pacproxy_server.start } + STDERR.puts 'Thread.new { @pacproxy_server.start } end' wait_server_status(@pacproxy_server, :Running) + STDERR.puts 'pacproxy_server started' + STDERR.puts 'HTTPClient creating' c = HTTPClient.new('http://127.0.0.1:13128') c.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE + STDERR.puts 'HTTPClient requesting 1' res = c.get('https://127.0.0.1:13443/') + STDERR.puts 'HTTPClient request 1 done' expect(res.status).to eq(200) + STDERR.puts 'HTTPClient requesting 2' res = c.get('https://127.0.0.1:13443/noproxy/') + STDERR.puts 'HTTPClient request 2 done' expect(res.status).to eq(200) + STDERR.puts 'transfer request to server directly via HTTPS exiting' + c.reset_all end it 'transfer request to server directly with PUT method' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 76191a9..0eab376 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,3 +14,5 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pacproxy' +require 'webrick' +require 'webrick/httpproxy'