├── .rspec ├── proxy.pac ├── model └── pacproxy.asta ├── box └── docker │ ├── proxy.pac │ ├── Gemfile │ ├── Dockerfile │ ├── README.md │ └── pacproxy.yml ├── spec ├── all_direct.pac ├── all_proxy.pac ├── partial_proxy.pac ├── runtime_spec.rb ├── spec_helper.rb ├── util_spec.rb ├── config_spec.rb ├── pac_file_spec.rb ├── access_logger_spec.rb ├── loggable_spec.rb └── pacproxy_spec.rb ├── Gemfile ├── lib ├── pacproxy │ ├── version.rb │ ├── runtimes │ │ ├── node │ │ │ ├── package.json │ │ │ ├── find.js │ │ │ └── node.rb │ │ ├── base.rb │ │ └── pac │ │ │ └── pac.rb │ ├── util.rb │ ├── general_logger.rb │ ├── config.rb │ ├── loggable.rb │ ├── runtime.rb │ ├── pac_file.rb │ ├── access_logger.rb │ └── pacproxy.rb └── pacproxy.rb ├── .rubocop.yml ├── pacproxy └── .gitignore ├── .travis.yml ├── Rakefile ├── .gitignore ├── LICENSE ├── pacproxy.gemspec ├── bin └── pacproxy ├── pacproxy.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /proxy.pac: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | return "DIRECT"; 3 | } 4 | -------------------------------------------------------------------------------- /model/pacproxy.asta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otahi/pacproxy/HEAD/model/pacproxy.asta -------------------------------------------------------------------------------- /box/docker/proxy.pac: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | return "DIRECT"; 3 | } 4 | -------------------------------------------------------------------------------- /spec/all_direct.pac: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | return "DIRECT"; 3 | } 4 | -------------------------------------------------------------------------------- /box/docker/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'pacproxy' 4 | gem 'therubyracer' 5 | -------------------------------------------------------------------------------- /spec/all_proxy.pac: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | return "PROXY localhost:13081"; 3 | } 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in pacproxy.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/pacproxy/version.rb: -------------------------------------------------------------------------------- 1 | # Pacproxy provides http/https proxy routed with proxy.pac. 2 | module Pacproxy 3 | VERSION = '0.0.10dev' 4 | end 5 | -------------------------------------------------------------------------------- /spec/partial_proxy.pac: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | if(shExpMatch ( url, "*noproxy*")) return "DIRECT"; 3 | return "PROXY localhost:13081"; 4 | } 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Avoid methods longer than 10 lines of code 2 | MethodLength: 3 | Enabled: false 4 | 5 | # Avoid classes longer than 100 lines of code 6 | ClassLength: 7 | Enabled: false 8 | -------------------------------------------------------------------------------- /pacproxy/.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | -------------------------------------------------------------------------------- /spec/runtime_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Pacproxy::Runtime do 4 | 5 | after(:each) do 6 | @runtime.shutdown 7 | end 8 | 9 | describe 'Runtime#new' do 10 | it 'returns Pacproxy::Runtime' do 11 | @runtime = Pacproxy::Runtime.new 12 | expect(@runtime).to be_kind_of(Pacproxy::Runtime) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/pacproxy.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy/version' 2 | require 'pacproxy/util' 3 | require 'pacproxy/loggable' 4 | require 'pacproxy/pacproxy' 5 | require 'pacproxy/pac_file' 6 | require 'pacproxy/general_logger' 7 | require 'pacproxy/access_logger' 8 | require 'pacproxy/config' 9 | 10 | require 'pacproxy/runtime' 11 | 12 | # Pacproxy provides http/https proxy routed with proxy.pac. 13 | module Pacproxy 14 | end 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | require 'simplecov' 5 | 6 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 7 | SimpleCov::Formatter::HTMLFormatter, 8 | Coveralls::SimpleCov::Formatter 9 | ] 10 | SimpleCov.start do 11 | add_filter '.bundle/' 12 | end 13 | 14 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 15 | 16 | require 'pacproxy' 17 | -------------------------------------------------------------------------------- /spec/util_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Pacproxy::Util do 4 | describe 'Util#which' do 5 | it 'return "/bin/sh" when "sh" is given' do 6 | l = Pacproxy::Util.which('sh') 7 | expect(l).to eq('/bin/sh') 8 | end 9 | it 'return nil when "unknown command" is given' do 10 | l = Pacproxy::Util.which('unknown command') 11 | expect(l).to be_nil 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | 4 | before_install: 5 | - 'test Node = "$PACPROXY_RUNTIME" && sudo apt-get update || true' 6 | - 'test Node = "$PACPROXY_RUNTIME" && sudo apt-get install node || true' 7 | - 'test Node = "$PACPROXY_RUNTIME" && (cd lib/pacproxy/runtimes/node ; npm install) || true' 8 | 9 | rvm: 10 | - 2.0.0 11 | - 2.1.4 12 | 13 | env: 14 | - PACPROXY_RUNTIME=Node 15 | - PACPROXY_RUNTIME= 16 | -------------------------------------------------------------------------------- /box/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.1.6 2 | MAINTAINER Hiroshi Ota 3 | 4 | RUN mkdir -p /opt/pacproxy 5 | RUN mkdir -p /opt/pacproxy/work 6 | WORKDIR /opt/pacproxy 7 | 8 | COPY Gemfile ./ 9 | RUN bundle install 10 | 11 | ADD pacproxy.yml /opt/pacproxy/work/pacproxy.yml 12 | ADD proxy.pac /opt/pacproxy/work/proxy.pac 13 | 14 | EXPOSE 3128 15 | 16 | WORKDIR /opt/pacproxy/work 17 | CMD pacproxy -c pacproxy.yml 18 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rubocop/rake_task' 3 | require 'rspec/core/rake_task' 4 | require 'coveralls/rake/task' 5 | 6 | task default: [:spec, 'coveralls:push', :rubocop] 7 | 8 | Coveralls::RakeTask.new 9 | 10 | RSpec::Core::RakeTask.new(:spec) do |t| 11 | t.pattern = 'spec/**/*_spec.rb' 12 | end 13 | 14 | RuboCop::RakeTask.new(:rubocop) do |task| 15 | task.patterns = %w(lib/**/*.rb spec/**/*.rb) 16 | end 17 | -------------------------------------------------------------------------------- /lib/pacproxy/runtimes/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pacproxy", 3 | "description": "A proxy server works with proxy.pac", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/otahi/pacproxy.git" 7 | }, 8 | "dependencies": { 9 | "dnode": "^1.2.0", 10 | "pac-resolver": "^1.2.2" 11 | }, 12 | "author": "Hiroshi OTA", 13 | "license": "MIT", 14 | "readme": "See: https://github.com/otahi/pacproxy" 15 | } 16 | -------------------------------------------------------------------------------- /lib/pacproxy/runtimes/base.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | 3 | module Pacproxy 4 | module Runtimes 5 | # Pacproxy::Runtimes::Basee represet basic runtime 6 | class Base 7 | include Loggable 8 | 9 | attr_reader :source 10 | 11 | def self.runtime 12 | end 13 | 14 | def initialize 15 | end 16 | 17 | def shutdown 18 | end 19 | 20 | def update(_file_location) 21 | end 22 | 23 | def find(_url) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/pacproxy/util.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | 3 | module Pacproxy 4 | # Pacproxy::PacUtil is utility 5 | class Util 6 | def self.which(cmd) 7 | exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] 8 | ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| 9 | exts.each do |ext| 10 | exe = File.join(path, "#{cmd}#{ext}") 11 | return exe if File.executable?(exe) && !File.directory?(exe) 12 | end 13 | end 14 | nil 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Pacproxy::Config do 4 | it 'store values from specified yaml file' do 5 | c = Pacproxy::Config.instance.update('pacproxy.yml') 6 | 7 | expect(c.config['daemonize']).to eq(true) 8 | expect(c.config['port']).to eq(3128) 9 | expect(c.config['pac_file']['location']).to eq('proxy.pac') 10 | expect(c.config['general_log']['location']).to eq('pacproxy.log') 11 | expect(c.config['access_log']['log_rotate']['shift_age']).to eq(7) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/pacproxy/runtimes/node/find.js: -------------------------------------------------------------------------------- 1 | var dnode = require('dnode'); 2 | var pac = require('pac-resolver'); 3 | var source; 4 | 5 | process.on('uncaughtException', function(err){}); 6 | 7 | var server = dnode({ 8 | find : function(source, uri, host, cb) { 9 | var FindProxyForURL = pac(source); 10 | FindProxyForURL(uri, host, function (err, res) { 11 | if (err) res = "DIRECT"; 12 | cb(res); 13 | }); 14 | } 15 | }); 16 | 17 | var port = process.argv[2]; 18 | server.listen(port); 19 | -------------------------------------------------------------------------------- /lib/pacproxy/general_logger.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | require 'logger' 3 | 4 | module Pacproxy 5 | # Provide log Function 6 | class GeneralLogger 7 | include Singleton 8 | 9 | attr_accessor :logger 10 | 11 | def initialize 12 | c = Config.instance.config['general_log'] 13 | return @logger = nil unless c 14 | 15 | location = c['location'] ? c['location'] : STDOUT 16 | shift_age = c['shift_age'] ? c['shift_age'] : 0 17 | shift_size = c['shift_size'] ? c['shift_size'] : 1_048_576 18 | @logger = Logger.new(location, shift_age, shift_size) 19 | @logger.level = c['log_level'] ? Logger.const_get(c['log_level']) : Logger::ERROR 20 | @logger.progname = 'pacproxy' 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/pacproxy/config.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | require 'yaml' 3 | 4 | module Pacproxy 5 | # Pacproxy::Config represent configuration for Pacproxy 6 | class Config 7 | include Singleton 8 | DEFAULT_CONFIG = { 9 | 'daemonize' => false, 10 | 'port' => 3128, 11 | 'pac_file' => { 'location' => nil }, 12 | 'general_log' => { 'location' => 'pacproxy.log' } 13 | } 14 | 15 | attr_reader :config 16 | 17 | def initialize 18 | @config = DEFAULT_CONFIG 19 | self 20 | end 21 | 22 | def update(yaml_file = 'pacproxy.yml') 23 | @config.merge!(read_config(yaml_file)) 24 | self 25 | end 26 | 27 | def read_config(yaml_file) 28 | return {} unless yaml_file 29 | return {} unless File.exist?(yaml_file) 30 | YAML.load(File.read(yaml_file)) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | tags 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalisation: 25 | /.bundle/ 26 | /lib/bundler/man/ 27 | /vendor/ 28 | /lib/pacproxy/runtimes/node/node_modules/ 29 | 30 | # for a library or gem, you might want to ignore these files since the code is 31 | # intended to run in multiple environments; otherwise, check them in: 32 | Gemfile.lock 33 | .ruby-version 34 | .ruby-gemset 35 | 36 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 37 | .rvmrc 38 | 39 | # for log files 40 | *.log 41 | 42 | # for backup files 43 | *.bak 44 | -------------------------------------------------------------------------------- /lib/pacproxy/loggable.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy/general_logger' 2 | require 'pacproxy/access_logger' 3 | 4 | module Pacproxy 5 | # Provide log Function 6 | module Loggable 7 | def general_logger 8 | GeneralLogger.instance.logger 9 | end 10 | 11 | def access_logger 12 | AccessLogger.instance 13 | end 14 | 15 | def debug(message) 16 | general_logger.debug(message) if general_logger 17 | end 18 | 19 | def info(message) 20 | general_logger.info(message) if general_logger 21 | end 22 | 23 | def lwarn(message) 24 | general_logger.warn(message) if general_logger 25 | end 26 | 27 | def error(message) 28 | general_logger.error(message) if general_logger 29 | end 30 | 31 | def fatal(message) 32 | general_logger.fatal(message) if general_logger 33 | end 34 | 35 | def accesslog(req, res) 36 | access_logger.accesslog(req, res) if access_logger 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/pacproxy/runtime.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | require 'pacproxy/runtimes/node/node' 3 | require 'pacproxy/runtimes/pac/pac' 4 | 5 | module Pacproxy 6 | # Pacproxy::Runtime represet runtime 7 | class Runtime 8 | include Loggable 9 | 10 | def initialize 11 | @runtime = autodetect 12 | end 13 | 14 | def shutdown 15 | @runtime.shutdown 16 | end 17 | 18 | def find(url) 19 | @runtime.find(url) 20 | end 21 | 22 | def update(file_location) 23 | @runtime.update(file_location) 24 | end 25 | 26 | private 27 | 28 | def autodetect 29 | name = ENV['PACPROXY_RUNTIME'] 30 | return Runtimes::Node.runtime if name || /Node/ =~ name 31 | 32 | ENV['JS_RUNTIME'] = name 33 | return Runtimes::Pac.runtime if Runtimes::Pac.runtime 34 | return Runtimes::Node.runtime if Runtimes::Node.runtime 35 | 36 | fail(RuntimeUnavailable, 37 | 'No runtime supporting proxy.pac') 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/pacproxy/runtimes/pac/pac.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | require 'pacproxy/runtimes/base' 3 | 4 | require 'open-uri' 5 | require 'thread' 6 | 7 | module Pacproxy 8 | module Runtimes 9 | # Pacproxy::Runtimes::Pac represent Pac 10 | class Pac < Base 11 | include Loggable 12 | 13 | attr_reader :source 14 | 15 | @js_lock = Mutex.new 16 | class << self 17 | attr_reader :js_lock 18 | end 19 | 20 | def self.runtime 21 | PAC.runtime 22 | new 23 | end 24 | 25 | def find(url) 26 | return 'DIRECT' unless @pac 27 | Pac.js_lock.synchronize do 28 | @pac.find(url) 29 | end 30 | end 31 | 32 | def update(file_location) 33 | Pac.js_lock.synchronize do 34 | tmp = PAC.load(file_location) 35 | @pac = tmp if @pac.nil? || @pac.source != tmp.source 36 | end 37 | rescue => e 38 | error("#{file_location} update error: #{e}") 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/pac_file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Pacproxy::PacFile do 4 | after(:each) do 5 | @pac_file.shutdown 6 | end 7 | 8 | describe 'PacFile#find' do 9 | it 'returns proxyurl in pac file' do 10 | @pac_file = Pacproxy::PacFile.new('spec/all_proxy.pac') 11 | expect(@pac_file.find('http://sample.org/')).to eq('PROXY localhost:13081') 12 | end 13 | it 'returns DIRECT when no pac file' do 14 | @pac_file = Pacproxy::PacFile.new('') 15 | expect(@pac_file.find('http://sample.org/')).to eq('DIRECT') 16 | end 17 | end 18 | describe 'PacFile#update' do 19 | it 'has same pac file if no change' do 20 | @pac_file = Pacproxy::PacFile.new('spec/all_proxy.pac', 0.01) 21 | expect(@pac_file.instance_variable_get('@runtime')) 22 | .to receive(:update).at_least(2).times 23 | 24 | first_pac = @pac_file.instance_variable_get(:@pac) 25 | sleep 0.2 26 | second_pac = @pac_file.instance_variable_get(:@pac) 27 | expect(second_pac).to eq(first_pac) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 OTA Hiroshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /box/docker/README.md: -------------------------------------------------------------------------------- 1 | # Pacproxy 2 | 3 | Docker images of [Pacproxy](https://github.com/otahi/pacproxy/). 4 | Pacproxy provides a proxy server controlled with your proxy.pac. 5 | 6 | ## Usage 7 | 8 | You can use this container as a proxy server which can be controlled with proxy pac. 9 | You can use Pacproxy server with `http_proxy` and `https_proxy` environment variables as same as usual proxy servers. 10 | 11 | ### For docker 12 | 13 | Run your container with your proxy pac location. 14 | 15 | ``` 16 | docker run -d -p 3128:3128 -it otahi/pacproxy pacproxy -P http://example.com/proxy.pac 17 | ``` 18 | or 19 | 20 | Put your pacproxy.yml with your own configuration on your currecnt directory. 21 | ``` 22 | docker run -d -p 3128:3128 -v`pwd`:/opt/pacproxy/work -it otahi/pacproxy 23 | ``` 24 | 25 | See [pacproxy.yml](pacproxy.yml). 26 | 27 | ## Contributing 28 | 29 | 1. Fork it ( https://github.com/otahi/pacproxy/fork ) 30 | 2. Create your feature branch (`git checkout -b my-new-feature`) 31 | 3. Commit your changes (`git commit -am 'Add some feature'`) 32 | 4. Push to the branch (`git push origin my-new-feature`) 33 | 5. Create a new Pull Request 34 | -------------------------------------------------------------------------------- /lib/pacproxy/pac_file.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | require 'uri' 3 | require 'thread' 4 | 5 | module Pacproxy 6 | # Pacproxy::PacFile represent proxy.pac file 7 | class PacFile 8 | include Loggable 9 | 10 | def initialize(file_location, update_interval = 1800) 11 | begin 12 | require 'pac' 13 | rescue PAC::RuntimeUnavailable 14 | info('No javascript runtime found for pac') 15 | end 16 | @file_location = file_location 17 | @update_interval = update_interval 18 | @runtime = Runtime.new 19 | begin_update 20 | end 21 | 22 | def shutdown 23 | @update_thread.kill if @update_thread 24 | @runtime.shutdown if @runtime 25 | end 26 | 27 | def find(uri) 28 | return 'DIRECT' unless @runtime 29 | @runtime.find(uri) 30 | end 31 | 32 | private 33 | 34 | def begin_update 35 | is_updated = false 36 | @update_thread = Thread.new do 37 | loop do 38 | @runtime.update(@file_location) 39 | is_updated = true 40 | sleep(@update_interval) 41 | end 42 | end 43 | sleep 0.01 until is_updated 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/pacproxy/access_logger.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | require 'logger' 3 | require 'webrick/accesslog' 4 | 5 | module Pacproxy 6 | # Provide log Function 7 | class AccessLogger 8 | include Singleton 9 | 10 | attr_accessor :logger 11 | 12 | def initialize 13 | c = Config.instance.config['access_log'] 14 | @format = WEBrick::AccessLog::COMMON_LOG_FORMAT 15 | return @logger = nil unless c 16 | 17 | @format = c['format'] if c['format'] 18 | 19 | location = c['location'] ? c['location'] : STDOUT 20 | shift_age = c['shift_age'] ? c['shift_age'] : 0 21 | shift_size = c['shift_size'] ? c['shift_size'] : 1_048_576 22 | @logger = Logger.new(location, shift_age, shift_size) 23 | end 24 | 25 | def accesslog(req, res) 26 | params = setup_params(req, res) 27 | return unless @logger 28 | @logger << WEBrick::AccessLog.format(@format, params) 29 | @logger << "\n" 30 | end 31 | 32 | private 33 | 34 | # This format specification is a subset of mod_log_config of Apache: 35 | # See: https://github.com/ruby/ruby/blob/trunk/lib/webrick/accesslog.rb 36 | 37 | def setup_params(req, res) 38 | WEBrick::AccessLog.setup_params({ ServerName: '-' }, req, res) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/access_logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Pacproxy::AccessLogger do 4 | describe 'accesslog' do 5 | it 'write Apache common log format' do 6 | Pacproxy::Config.instance.update('pacproxy.yml') 7 | 8 | log = Pacproxy::AccessLogger.instance 9 | log.logger = '' 10 | now = Time.now 11 | now_string = now.strftime('[%d/%b/%Y:%H:%M:%S %Z]') 12 | 13 | req = double('req') 14 | allow(req).to receive(:attributes).and_return([]) 15 | allow(req).to receive(:peeraddr).and_return(%w(host-a host-b host-c)) 16 | allow(req).to receive(:port).and_return(80) 17 | allow(req).to receive(:query_string).and_return('query_string_test') 18 | allow(req).to receive(:request_line) 19 | .and_return(req_line = 'GET http://remotehost/abc HTTP/1.1') 20 | allow(req).to receive(:request_method).and_return('GET') 21 | allow(req).to receive(:request_time).and_return(now) 22 | allow(req).to receive(:unparsed_uri).and_return('http://remotehost/abc') 23 | allow(req).to receive(:user).and_return('user-a') 24 | 25 | res = double('req') 26 | allow(res).to receive(:filename).and_return('') 27 | allow(res).to receive(:sent_size).and_return(128) 28 | allow(res).to receive(:status).and_return(200) 29 | 30 | log.accesslog(req, res) 31 | expect(log.logger) 32 | .to eq("host-c - user-a #{now_string} \"#{req_line}\" 200 128\n") 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/loggable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Pacproxy::GeneralLogger do 4 | 5 | # Loggable class example 6 | class LoggableExample 7 | include Pacproxy::Loggable 8 | end 9 | 10 | before(:each) do 11 | log = StringIO.new 12 | @logger = Logger.new(log) 13 | @logger.level = Logger::DEBUG 14 | Pacproxy::GeneralLogger.instance.logger = @logger 15 | @loggable = LoggableExample.new 16 | end 17 | 18 | describe '#debug' do 19 | it 'write debug log' do 20 | message = 'DEBUG LOG' 21 | expect(@logger).to receive(:debug).with(message) 22 | @loggable.debug(message) 23 | end 24 | end 25 | describe '#info' do 26 | it 'write info log' do 27 | message = 'INFO LOG' 28 | expect(@logger).to receive(:info).with(message) 29 | @loggable.info(message) 30 | end 31 | end 32 | describe '#lwarn' do 33 | it 'write warn log' do 34 | message = 'WARN LOG' 35 | expect(@logger).to receive(:warn).with(message) 36 | @loggable.lwarn(message) 37 | end 38 | end 39 | describe '#error' do 40 | it 'write error log' do 41 | message = 'ERROR LOG' 42 | expect(@logger).to receive(:error).with(message) 43 | @loggable.error(message) 44 | end 45 | end 46 | describe '#fatal' do 47 | it 'write fatal log' do 48 | message = 'FATAL LOG' 49 | expect(@logger).to receive(:fatal).with(message) 50 | @loggable.fatal(message) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /pacproxy.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'pacproxy/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "pacproxy" 8 | spec.version = Pacproxy::VERSION 9 | spec.authors = ["OTA Hiroshi"] 10 | spec.email = ["otahi.pub@gmail.com"] 11 | spec.summary = %q{A proxy server works with proxy.pac} 12 | spec.description = %q{A proxy server works with proxy.pac} 13 | spec.homepage = "https://github.com/otahi/pacproxy" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency 'pac', '~> 1.0.0' 22 | spec.add_dependency 'dnode', '~> 0.0.2' 23 | spec.add_dependency 'os', '~> 0.9.6' 24 | if /cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM 25 | spec.add_dependency 'win32-process', '~> 0.7.4' 26 | end 27 | 28 | spec.add_development_dependency 'bundler', '~> 1.6' 29 | spec.add_development_dependency 'rspec', '~> 3.0.0' 30 | spec.add_development_dependency 'rake', '~> 10.3.2' 31 | spec.add_development_dependency 'httpclient', '~> 2.4.0' 32 | spec.add_development_dependency 'therubyracer', '~> 0.12.1' 33 | 34 | spec.add_development_dependency 'rubocop', '0.24.1' 35 | spec.add_development_dependency 'coveralls', '~> 0.7' 36 | spec.add_development_dependency 'byebug', '~> 3.4.0' 37 | end 38 | -------------------------------------------------------------------------------- /bin/pacproxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pacproxy' 4 | require 'optparse' 5 | require 'logger' 6 | 7 | config_file = nil 8 | config = Pacproxy::Config.instance.config 9 | 10 | OptionParser.new do |o| 11 | o.on('-c CONFIGFILE', String, 12 | "specify config file. default: #{config_file}") do |f| 13 | config_file = f 14 | config = Pacproxy::Config.instance.update(config_file).config 15 | end 16 | o.on('-d', 'daemonize') { config.merge!('daemonize' => true) } 17 | o.on('-l LOGFILE', String, 18 | "specify log file. default: #{config['log_file']}") do |l| 19 | config.merge!('log_file' => l) 20 | end 21 | o.on('-p PORT', Integer, 22 | "specify listening port. default: #{config['port']}") do |p| 23 | config.merge!('port' => p) 24 | end 25 | o.on('-P PROXYPAC', String, 'specify proxy.pac location') do |pac| 26 | config.merge!('pac_file' => { 'location' => pac }) 27 | end 28 | o.on('--npminstall', 'install node libraries') do 29 | fail 'No npm found!' unless Pacproxy::Util.which('npm') 30 | 31 | node_dir = File.join([File.dirname(__FILE__)] + 32 | %w(.. lib pacproxy runtimes node)) 33 | FileUtils.cd(node_dir) do 34 | system('npm install') 35 | end 36 | exit 37 | end 38 | o.on('-h', 'show this help') do 39 | puts o 40 | exit 41 | end 42 | o.parse! 43 | end 44 | 45 | s = Pacproxy::Pacproxy.new(config) 46 | 47 | Signal.trap('INT') do 48 | s.shutdown 49 | end 50 | 51 | if config['daemonize'] 52 | # To use executed directory as the current directory 53 | # The current directory is changed when daemonized to '/' 54 | Pacproxy::GeneralLogger.instance 55 | Pacproxy::AccessLogger.instance 56 | 57 | WEBrick::Daemon.start { s.start } 58 | else 59 | s.start 60 | end 61 | -------------------------------------------------------------------------------- /pacproxy.yml: -------------------------------------------------------------------------------- 1 | # pacproxy conifguration 2 | 3 | # daemonize: [boolean] 4 | # eg. daemonize: true 5 | daemonize: true 6 | 7 | # port: [integer] 8 | # eg. port: 3128 9 | port: 3128 10 | 11 | # pac_file related items 12 | pac_file: 13 | # pac_file > location: [string] 14 | # absolute path is preferred 15 | # eg. location: /opt/pacproxy/proxy.pac 16 | # eg. location: http://example.com/proxy.pac 17 | location: proxy.pac 18 | # pac_file > update_interval(seconds): [integer] 19 | # eg. update_interval: 1800 20 | update_interval: 1800 21 | 22 | # upstream proxy authorization related items 23 | # auth > user: [string] 24 | # user name for upstream proxy server 25 | # auth > password: [string] 26 | # password for upstream proxy server 27 | # eg. 28 | # auth: 29 | # user: user100 30 | # password: abcd1234 31 | 32 | # general log related items 33 | general_log: 34 | # general_log > location: [string] 35 | # absolute path is preferred 36 | # eg. location: /var/log/pacproxy.log 37 | location: pacproxy.log 38 | # general_log > log_level: [string] 39 | # DEBUG/INFO/WARN/ERROR/FATAL 40 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 41 | # eg. log_level: ERROR 42 | log_level: ERROR 43 | # general_log > log_rotate related items 44 | log_rotate: 45 | # general_log > log_rotate > shift_age: [integer/string] 46 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 47 | shift_age: 7 48 | # general_log > log_rotate > shift_size: [integer] 49 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 50 | shift_size: 10485760 #10 * 1024 * 1024 51 | 52 | # access log related items 53 | access_log: 54 | # access_log > location: [string] 55 | # absolute path is preferred 56 | # eg. location: /var/log/proxy_access.log 57 | location: proxy_access.log 58 | # access_log > format: [string] 59 | # eg. format: "%h %l %u %t \"%r\" %s %b" 60 | # see: http://httpd.apache.org/docs/current/en/mod/mod_log_config.html 61 | format: "%h %l %u %t \"%r\" %s %b" 62 | # access_log > log_rotate related items 63 | log_rotate: 64 | # access_log > log_rotate > shift_age: [integer/string] 65 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 66 | shift_age: 7 67 | # access_log > log_rotate > shift_size: [integer] 68 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 69 | shift_size: 10485760 #10 * 1024 * 1024 70 | -------------------------------------------------------------------------------- /box/docker/pacproxy.yml: -------------------------------------------------------------------------------- 1 | # pacproxy conifguration 2 | 3 | # daemonize: [boolean] 4 | # eg. daemonize: true 5 | # If you run this on docker it should be false 6 | daemonize: false 7 | 8 | # port: [integer] 9 | # eg. port: 3128 10 | port: 3128 11 | 12 | # pac_file related items 13 | pac_file: 14 | # pac_file > location: [string] 15 | # absolute path is preferred 16 | # eg. location: /opt/pacproxy/proxy.pac 17 | # eg. location: http://example.com/proxy.pac 18 | location: proxy.pac 19 | # pac_file > update_interval(seconds): [integer] 20 | # eg. update_interval: 1800 21 | update_interval: 1800 22 | 23 | # upstream proxy authorization related items 24 | # auth > user: [string] 25 | # user name for upstream proxy server 26 | # auth > password: [string] 27 | # password for upstream proxy server 28 | # eg. 29 | # auth: 30 | # user: user100 31 | # password: abcd1234 32 | 33 | # general log related items 34 | general_log: 35 | # general_log > location: [string] 36 | # absolute path is preferred 37 | # eg. location: /var/log/pacproxy.log 38 | location: pacproxy.log 39 | # general_log > log_level: [string] 40 | # DEBUG/INFO/WARN/ERROR/FATAL 41 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 42 | # eg. log_level: ERROR 43 | log_level: ERROR 44 | # general_log > log_rotate related items 45 | log_rotate: 46 | # general_log > log_rotate > shift_age: [integer/string] 47 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 48 | shift_age: 7 49 | # general_log > log_rotate > shift_size: [integer] 50 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 51 | shift_size: 10485760 #10 * 1024 * 1024 52 | 53 | # access log related items 54 | access_log: 55 | # access_log > location: [string] 56 | # absolute path is preferred 57 | # eg. location: /var/log/proxy_access.log 58 | location: proxy_access.log 59 | # access_log > format: [string] 60 | # eg. format: "%h %l %u %t \"%r\" %s %b" 61 | # see: http://httpd.apache.org/docs/current/en/mod/mod_log_config.html 62 | format: "%h %l %u %t \"%r\" %s %b" 63 | # access_log > log_rotate related items 64 | log_rotate: 65 | # access_log > log_rotate > shift_age: [integer/string] 66 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 67 | shift_age: 7 68 | # access_log > log_rotate > shift_size: [integer] 69 | # see: http://ruby-doc.org/stdlib-2.1.2/libdoc/logger/rdoc/Logger.html 70 | shift_size: 10485760 #10 * 1024 * 1024 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pacproxy [![Build Status](https://travis-ci.org/otahi/pacproxy.png?branch=master)](https://travis-ci.org/otahi/pacproxy)[![Coverage Status](https://coveralls.io/repos/otahi/pacproxy/badge.png?branch=master)](https://coveralls.io/r/otahi/pacproxy?branch=master)[![Code Climate](https://codeclimate.com/github/otahi/pacproxy.png)](https://codeclimate.com/github/otahi/pacproxy)[![Gem Version](https://badge.fury.io/rb/pacproxy.png)](http://badge.fury.io/rb/pacproxy) 2 | 3 | Pacproxy provides an http/https proxy server which does proxy access according with a local/remote proxy.pac. 4 | If your user agent is behind of the corporate proxy server and it does not recognize proxy.pac, 5 | Proxypac transfers both your Internet and Intranet access correctly. 6 | 7 | ## Usage 8 | 9 | You can run pacproxy with specified proxy.pac location, running port and so on. 10 | 11 | $ bundle exec pacproxy -P http://sample.org/proxy.pac -p 3128 12 | 13 | or 14 | 15 | $ bundle exec pacproxy -P /opt/pacproxy/sample-proxy.pac -p 3128 16 | 17 | or 18 | 19 | $ bundle exec pacproxy -c pacproxy.yml 20 | 21 | Or, you can run this as a docker container. See [Readme for Docker](./box/docker/README.md). 22 | 23 | ## Configuration 24 | 25 | You can configure pacproxy by a file which you specified with `-c` option. 26 | The default configuration file is `pacproxy.yml`([sample](./pacproxy.yml)) 27 | in the current working directory. 28 | 29 | Configurable items: 30 | - daemonize 31 | - port 32 | - pac file 33 | - general log 34 | - access log 35 | 36 | ## Installation 37 | You can select Ruby javascript runtimes or Node.js 38 | 39 | ### With a Ruby javascript runtime 40 | Puts these lines on Gemfile, for example: 41 | 42 | source 'https://rubygems.org' 43 | 44 | gem 'pacproxy' 45 | gem 'therubyracer' 46 | 47 | And then execute: 48 | 49 | $ bundle 50 | 51 | ### With Node.js 52 | Install node.js runtime before this installation. 53 | Puts these lines on Gemfile, for example: 54 | 55 | source 'https://rubygems.org' 56 | 57 | gem 'pacproxy' 58 | 59 | And then execute: 60 | 61 | $ bundle 62 | $ bundle exec pacproxy --npminstall 63 | 64 | ## Requirements 65 | 66 | Before or After installing the `pacproxy` gem, 67 | you need to install a JavaScript runtime.: 68 | 69 | * [therubyracer](https://rubygems.org/gems/therubyracer) Google V8 embedded within Ruby 70 | * [therubyrhino](https://rubygems.org/gems/therubyrhino/) Mozilla Rhino embedded within JRuby 71 | * [johnson](https://rubygems.org/gems/johnson/) Mozilla SpiderMonkey embedded within Ruby 1.8 72 | * [mustang](https://rubygems.org/gems/mustang/) Mustang V8 embedded within Ruby 73 | * [Node.js](http://nodejs.org/) Node.js runtime 74 | 75 | ## Contributing 76 | 77 | 1. Fork it ( https://github.com/otahi/pacproxy/fork ) 78 | 2. Create your feature branch (`git checkout -b my-new-feature`) 79 | 3. Commit your changes (`git commit -am 'Add some feature'`) 80 | 4. Push to the branch (`git push origin my-new-feature`) 81 | 5. Create a new Pull Request 82 | -------------------------------------------------------------------------------- /lib/pacproxy/runtimes/node/node.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | require 'pacproxy/runtimes/base' 3 | 4 | require 'open-uri' 5 | require 'dnode' 6 | require 'thread' 7 | require 'monitor' 8 | require 'os' 9 | 10 | module Pacproxy 11 | module Runtimes 12 | # Pacproxy::Runtimes::Node represet node js runtime 13 | class Node < Base 14 | include Loggable 15 | 16 | TIMEOUT_JS_CALL = 0.5 17 | TIMEOUT_JS_SERVER = 5 18 | attr_reader :source 19 | 20 | def self.runtime 21 | if Util.which('node').nil? 22 | error('No PAC supported runtime') 23 | fail(RuntimeUnavailable, 24 | 'No PAC supported runtime') 25 | end 26 | new 27 | end 28 | 29 | def initialize 30 | js = File.join(File.dirname(__FILE__), 'find.js') 31 | 32 | retries = 3 33 | begin 34 | Timeout.timeout(TIMEOUT_JS_SERVER) do 35 | server = TCPServer.new('127.0.0.1', 0) 36 | @port = server.addr[1] 37 | server.close 38 | if OS.windows? 39 | @server_pid = start_server 40 | else 41 | @server_pid = fork { exec('node', js, @port.to_s) } 42 | Process.detach(@server_pid) 43 | end 44 | sleep 0.01 until port_open? 45 | 46 | initialize_client 47 | end 48 | rescue Timeout::Error 49 | shutdown 50 | if retries > 0 51 | retries -= 1 52 | lwarn('Timeout. Initialize Node.js server.') 53 | retry 54 | else 55 | error('Gave up to retry Initialize Node.js server.') 56 | raise 'Gave up to retry Initialize Node.js server.' 57 | end 58 | end 59 | end 60 | 61 | def shutdown 62 | @client_thread.kill if @client_thread 63 | if OS.windows? 64 | stop_server(@server_pid) 65 | else 66 | Process.kill(:INT, @server_pid) 67 | end 68 | end 69 | 70 | def update(file_location) 71 | @source = open(file_location, proxy: false).read 72 | rescue 73 | @source = nil 74 | end 75 | 76 | def find(url) 77 | return 'DIRECT' unless @source 78 | uri = URI.parse(url) 79 | call_find(uri) 80 | end 81 | 82 | private 83 | 84 | def initialize_client 85 | @queue = Queue.new 86 | @client_thread = Thread.new do 87 | DNode.new.connect('127.0.0.1', @port) do |remote| 88 | q = @queue.pop 89 | if q[:uri] && q[:uri].host && q[:call_back] 90 | remote.find(@source, q[:uri], q[:uri].host, q[:call_back]) 91 | end 92 | end 93 | end 94 | end 95 | 96 | def port_open? 97 | Timeout.timeout(TIMEOUT_JS_CALL) do 98 | begin 99 | TCPSocket.new('127.0.0.1', @port).close 100 | return true 101 | rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH 102 | return false 103 | end 104 | end 105 | rescue Timeout::Error 106 | false 107 | end 108 | 109 | def call_find(uri, retries = 3) 110 | proxy = nil 111 | begin 112 | mon = Monitor.new 113 | cond = mon.new_cond 114 | thread = Thread.new do 115 | mon.synchronize do 116 | @queue.push(uri: uri, 117 | call_back: proc do |p| 118 | proxy = p 119 | cond.signal 120 | end) 121 | cond.wait 122 | end 123 | end 124 | thread.join(TIMEOUT_JS_CALL) 125 | proxy 126 | rescue Timeout::Error 127 | if retries > 0 128 | retries -= 1 129 | lwarn('Timeout. Retring call_find.') 130 | retry 131 | else 132 | error('Gave up Retry call_find.') 133 | nil 134 | end 135 | end 136 | end 137 | 138 | def rand_string 139 | (0...16).map { ('a'..'z').to_a[rand(26)] }.join 140 | end 141 | 142 | def start_server 143 | require 'win32/process' 144 | Process.create( 145 | app_name: Util.which('node'), 146 | creation_flags: Process::DETACHED_PROCESS 147 | ) 148 | end 149 | 150 | def stop_server(server_info) 151 | require 'win32/process' 152 | return unless server_info || server_info.respond_to?(:process_id) 153 | Process.kill('ExitProcess', [server_info.process_id]) 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/pacproxy/pacproxy.rb: -------------------------------------------------------------------------------- 1 | require 'pacproxy' 2 | require 'webrick/httpproxy' 3 | require 'uri' 4 | 5 | module Pacproxy 6 | # Pacproxy::Pacproxy represent http/https proxy server 7 | class Pacproxy < WEBrick::HTTPProxyServer # rubocop:disable ClassLength 8 | include Loggable 9 | 10 | def initialize(config = {}, default = WEBrick::Config::HTTP) 11 | super({ Port: config['port'], Logger: general_logger }, default) 12 | @auth = config['auth'] 13 | return unless config['pac_file'] && config['pac_file']['location'] 14 | 15 | @pac = PacFile.new(config['pac_file']['location'], 16 | config['pac_file']['update_interval']) 17 | end 18 | 19 | def shutdown 20 | @pac.shutdown if @pac 21 | super 22 | end 23 | 24 | def proxy_uri(req, res) 25 | super(req, res) 26 | return unless @pac 27 | 28 | proxy_line = @pac.find(request_uri(req)) 29 | proxy = lookup_proxy_uri(proxy_line) 30 | create_proxy_uri(proxy, req.header) 31 | end 32 | 33 | def create_proxy_uri(proxy, header) 34 | return nil unless proxy 35 | return URI.parse("http://#{proxy}") unless 36 | @auth || header.key?('proxy-authorization') 37 | 38 | if @auth 39 | basic_auth = "#{@auth['user']}:#{@auth['password']}" 40 | elsif header.key?('proxy-authorization') 41 | auth = header['proxy-authorization'][0] 42 | pattern = /basic (\S+)/i 43 | basic_auth = pattern.match(auth)[1].unpack('m').first 44 | header.delete('proxy-authorization') 45 | end 46 | 47 | URI.parse("http://#{basic_auth}@#{proxy}") 48 | end 49 | 50 | # This method is mainly from WEBrick::HTTPProxyServer. 51 | # To allow upstream proxy authentication, 52 | # it operate 407 response from an upstream proxy. 53 | # see: https://github.com/ruby/ruby/blob/trunk/lib/webrick/httpproxy.rb 54 | # rubocop:disable all 55 | def do_CONNECT(req, res) 56 | # Proxy Authentication 57 | proxy_auth(req, res) 58 | 59 | ua = Thread.current[:WEBrickSocket] # User-Agent 60 | raise WEBrick::HTTPStatus::InternalServerError, 61 | "[BUG] cannot get socket" unless ua 62 | 63 | host, port = req.unparsed_uri.split(":", 2) 64 | # Proxy authentication for upstream proxy server 65 | if proxy = proxy_uri(req, res) 66 | proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0" 67 | if proxy.userinfo 68 | credentials = "Basic " + [proxy.userinfo].pack("m").delete("\n") 69 | end 70 | host, port = proxy.host, proxy.port 71 | end 72 | 73 | begin 74 | @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.") 75 | os = TCPSocket.new(host, port) # origin server 76 | 77 | if proxy 78 | @logger.debug("CONNECT: sending a Request-Line") 79 | os << proxy_request_line << WEBrick::CRLF 80 | @logger.debug("CONNECT: > #{proxy_request_line}") 81 | if credentials 82 | @logger.debug("CONNECT: sending a credentials") 83 | os << "Proxy-Authorization: " << credentials << WEBrick::CRLF 84 | end 85 | os << WEBrick::CRLF 86 | proxy_status_line = os.gets(WEBrick::LF) 87 | @logger.debug("CONNECT: read a Status-Line form the upstream server") 88 | @logger.debug("CONNECT: < #{proxy_status_line}") 89 | if /^HTTP\/\d+\.\d+\s+(?200|407)\s*/ =~ proxy_status_line 90 | res.status = st.to_i 91 | while line = os.gets(WEBrick::LF) 92 | res.header['Proxy-Authenticate'] = 93 | line.split(':')[1] if /Proxy-Authenticate/i =~ line 94 | break if /\A(#{WEBrick::CRLF}|#{WEBrick::LF})\z/om =~ line 95 | end 96 | else 97 | raise WEBrick::HTTPStatus::BadGateway 98 | end 99 | end 100 | @logger.debug("CONNECT #{host}:#{port}: succeeded") 101 | rescue => ex 102 | @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'") 103 | res.set_error(ex) 104 | raise WEBrick::HTTPStatus::EOFError 105 | ensure 106 | if handler = @config[:ProxyContentHandler] 107 | handler.call(req, res) 108 | end 109 | res.send_response(ua) 110 | accesslog(req, res) 111 | 112 | # Should clear request-line not to send the response twice. 113 | # see: HTTPServer#run 114 | req.parse(WEBrick::NullReader) rescue nil 115 | end 116 | 117 | begin 118 | while fds = IO::select([ua, os]) 119 | if fds[0].member?(ua) 120 | buf = ua.sysread(1024); 121 | @logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent") 122 | os.syswrite(buf) 123 | elsif fds[0].member?(os) 124 | buf = os.sysread(1024); 125 | @logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}") 126 | ua.syswrite(buf) 127 | end 128 | end 129 | rescue 130 | os.close 131 | @logger.debug("CONNECT #{host}:#{port}: closed") 132 | end 133 | 134 | raise WEBrick::HTTPStatus::EOFError 135 | end 136 | # rubocop:enable all 137 | 138 | def proxy_auth(req, res) 139 | @config[:ProxyAuthProc].call(req, res) if @config[:ProxyAuthProc] 140 | end 141 | 142 | private 143 | 144 | def request_uri(request) 145 | if 'CONNECT' == request.request_method 146 | "https://#{request.unparsed_uri}/" 147 | else 148 | request.unparsed_uri 149 | end 150 | end 151 | 152 | def lookup_proxy_uri(proxy_line) 153 | case proxy_line 154 | when /^DIRECT/ 155 | nil 156 | when /PROXY/ 157 | primary_proxy = proxy_line.split(';')[0] 158 | /PROXY (.*)/.match(primary_proxy)[1] 159 | end 160 | end 161 | 162 | # This method is mainly from WEBrick::HTTPProxyServer. 163 | # proxy-authenticate can be transferred from a upstream proxy server 164 | # to a client 165 | # see: https://github.com/ruby/ruby/blob/trunk/lib/webrick/httpproxy.rb 166 | HOP_BY_HOP = %w( connection keep-alive upgrade 167 | proxy-authorization te trailers transfer-encoding ) 168 | SHOULD_NOT_TRANSFER = %w( set-cookie proxy-connection ) 169 | def choose_header(src, dst) 170 | connections = split_field(src['connection']) 171 | src.each do |key, value| 172 | key = key.downcase 173 | next if HOP_BY_HOP.member?(key) || # RFC2616: 13.5.1 174 | connections.member?(key) || # RFC2616: 14.10 175 | SHOULD_NOT_TRANSFER.member?(key) # pragmatics 176 | 177 | dst[key] = value 178 | end 179 | end 180 | 181 | def perform_proxy_request(req, res) 182 | super 183 | accesslog(req, res) 184 | end 185 | 186 | # allow PUT method on proxy server 187 | # method names for webrick is indicated by rubocop 188 | # rubocop:disable all 189 | def do_PUT(req, res) 190 | perform_proxy_request(req, res) do |http, path, header| 191 | http.put(path, req.body || '', header) 192 | end 193 | end 194 | # rubocop:enable all 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /spec/pacproxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'httpclient' 3 | require 'webrick/https' 4 | 5 | def wait_server_status(servers, status) 6 | return unless servers || status 7 | servers = [servers] unless servers.respond_to?(:all?) 8 | return unless servers.all? { |s| s.respond_to?(:status) } 9 | sleep(0.01) until servers.all? { |s| s.status == status } 10 | end 11 | 12 | describe Pacproxy do 13 | describe 'Pacproxy::VERSION' do 14 | it 'have a version number' do 15 | expect(Pacproxy::VERSION).not_to be_nil 16 | end 17 | end 18 | 19 | describe 'Pacproxy#proxy_uri' do 20 | before(:each) do 21 | $stdout, $stderr = StringIO.new, StringIO.new 22 | @http_server = WEBrick::HTTPServer.new(Port: 13_080) 23 | @http_server.define_singleton_method(:service) do |_req, res| 24 | res.status = 200 25 | end 26 | 27 | @https_server = WEBrick::HTTPServer.new(Port: 13_443, 28 | SSLEnable: true, 29 | SSLCertName: [%w(CN 127.0.0.1)]) 30 | @https_server.define_singleton_method(:service) do |_req, res| 31 | res.status = 200 32 | end 33 | 34 | @proxy_server = WEBrick::HTTPProxyServer.new(Port: 13_081) 35 | Thread.new { @http_server.start } 36 | Thread.new { @https_server.start } 37 | Thread.new { @proxy_server.start } 38 | wait_server_status([@http_server, @https_server, @proxy_server], :Running) 39 | end 40 | 41 | after(:each) do 42 | $stdout, $stderr = STDOUT, STDERR 43 | @http_server.shutdown 44 | @https_server.shutdown 45 | @proxy_server.shutdown 46 | @pacproxy_server.shutdown 47 | wait_server_status([@http_server, 48 | @https_server, 49 | @proxy_server, 50 | @pacproxy_server], 51 | :Stop) 52 | end 53 | 54 | it 'transfer request to server directly' do 55 | c = Pacproxy::Config.instance.config 56 | c['port'] = 13_128 57 | c['pac_file']['location'] = 'spec/all_direct.pac' 58 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 59 | Thread.new { @pacproxy_server.start } 60 | wait_server_status(@pacproxy_server, :Running) 61 | 62 | c = HTTPClient.new('http://127.0.0.1:13128') 63 | res = c.get('http://127.0.0.1:13080/') 64 | expect(res.status).to eq(200) 65 | 66 | res = c.get('http://127.0.0.1:13080/noproxy/') 67 | expect(res.status).to eq(200) 68 | end 69 | 70 | it 'transfer request to server directly via HTTPS' do 71 | c = Pacproxy::Config.instance.config 72 | c['port'] = 13_128 73 | c['pac_file']['location'] = 'spec/all_direct.pac' 74 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 75 | Thread.new { @pacproxy_server.start } 76 | wait_server_status(@pacproxy_server, :Running) 77 | 78 | c = HTTPClient.new('http://127.0.0.1:13128') 79 | c.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE 80 | res = c.get('https://127.0.0.1:13443/') 81 | expect(res.status).to eq(200) 82 | 83 | res = c.get('https://127.0.0.1:13443/noproxy/') 84 | expect(res.status).to eq(200) 85 | end 86 | 87 | it 'transfer request to server directly with PUT method' do 88 | c = Pacproxy::Config.instance.config 89 | c['port'] = 13_128 90 | c['pac_file']['location'] = 'spec/all_direct.pac' 91 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 92 | Thread.new { @pacproxy_server.start } 93 | wait_server_status(@pacproxy_server, :Running) 94 | 95 | c = HTTPClient.new('http://127.0.0.1:13128') 96 | res = c.put('http://127.0.0.1:13080/') 97 | expect(res.status).to eq(200) 98 | 99 | res = c.put('http://127.0.0.1:13080/noproxy/') 100 | expect(res.status).to eq(200) 101 | end 102 | 103 | it 'transfer request to server via parent proxy' do 104 | c = Pacproxy::Config.instance.config 105 | c['port'] = 13_128 106 | c['pac_file']['location'] = 'spec/all_direct.pac' 107 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 108 | Thread.new { @pacproxy_server.start } 109 | wait_server_status(@pacproxy_server, :Running) 110 | 111 | c = HTTPClient.new('http://127.0.0.1:13128') 112 | res = c.get('http://127.0.0.1:13080/') 113 | expect(res.status).to eq(200) 114 | 115 | res = c.get('http://127.0.0.1:13080/noproxy/') 116 | expect(res.status).to eq(200) 117 | end 118 | 119 | it 'transfer request to server via parent proxy partially' do 120 | c = Pacproxy::Config.instance.config 121 | c['port'] = 13_128 122 | c['pac_file']['location'] = 'spec/partial_proxy.pac' 123 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 124 | Thread.new { @pacproxy_server.start } 125 | wait_server_status(@pacproxy_server, :Running) 126 | 127 | c = HTTPClient.new('http://127.0.0.1:13128') 128 | res = c.get('http://127.0.0.1:13080/') 129 | expect(res.status).to eq(200) 130 | 131 | res = c.get('http://127.0.0.1:13080/noproxy/') 132 | expect(res.status).to eq(200) 133 | end 134 | 135 | it 'transfer request with auth to server via parent proxy' do 136 | c = Pacproxy::Config.instance.config 137 | c['port'] = 13_128 138 | c['pac_file']['location'] = 'spec/all_proxy.pac' 139 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 140 | 141 | Thread.new { @pacproxy_server.start } 142 | wait_server_status(@pacproxy_server, :Running) 143 | 144 | c = HTTPClient.new('http://127.0.0.1:13128') 145 | header = { header: { 'proxy-authorization' => 146 | %Q(Basic #{['user01:pass01'].pack('m').delete("\n")}) 147 | } 148 | } 149 | res = c.get('http://127.0.0.1:13080/', header) 150 | expect(res.status).to eq(200) 151 | res = c.get('http://127.0.0.1:13080/noproxy/', header) 152 | expect(res.status).to eq(200) 153 | end 154 | 155 | it 'transfer request with overridden auth to server via parent proxy' do 156 | auth = nil 157 | proxy_proc = proc do |req, _resp| 158 | auth = req.header['proxy-authorization'] 159 | end 160 | 161 | pc = @proxy_server.instance_variable_get('@config') 162 | @proxy_server.instance_variable_set('@config', 163 | pc.merge(ProxyAuthProc: proxy_proc)) 164 | 165 | c = Pacproxy::Config.instance.config 166 | c['port'] = 13_128 167 | c['pac_file']['location'] = 'spec/partial_proxy.pac' 168 | c['auth'] = { 'user' => 'user01', 'password' => 'pass01' } 169 | 170 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 171 | Thread.new { @pacproxy_server.start } 172 | wait_server_status(@pacproxy_server, :Running) 173 | 174 | c = HTTPClient.new('http://127.0.0.1:13128') 175 | res = c.get('http://127.0.0.1:13080/') 176 | expect(res.status).to eq(200) 177 | expect(auth) 178 | .to eq([%Q(Basic #{['user01:pass01'].pack('m').delete("\n")})]) 179 | end 180 | 181 | it 'respond 407 when upstrem proxy respond 407 on http' do 182 | proxy_proc = proc do |_req, resp| 183 | resp.header.merge!('Proxy-Authenticate' => "Basic realm=\"proxy\"") 184 | fail WEBrick::HTTPStatus::ProxyAuthenticationRequired 185 | end 186 | 187 | pc = @proxy_server.instance_variable_get('@config') 188 | @proxy_server.instance_variable_set('@config', 189 | pc.merge(ProxyAuthProc: proxy_proc)) 190 | c = Pacproxy::Config.instance.config 191 | c['port'] = 13_128 192 | c['pac_file']['location'] = 'spec/all_proxy.pac' 193 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 194 | 195 | Thread.new { @pacproxy_server.start } 196 | wait_server_status(@pacproxy_server, :Running) 197 | 198 | c = HTTPClient.new('http://127.0.0.1:13128') 199 | res = c.get('http://127.0.0.1:13080/') 200 | expect(res.status).to eq(407) 201 | expect(res.header['Proxy-Authenticate']).to eq(["Basic realm=\"proxy\""]) 202 | end 203 | 204 | it 'respond 407 when upstrem proxy respond 407 on https' do 205 | proxy_proc = proc do |_req, resp| 206 | resp.header.merge!('Proxy-Authenticate' => "Basic realm=\"proxy\"") 207 | fail WEBrick::HTTPStatus::ProxyAuthenticationRequired 208 | end 209 | 210 | pc = @proxy_server.instance_variable_get('@config') 211 | @proxy_server.instance_variable_set('@config', 212 | pc.merge(ProxyAuthProc: proxy_proc)) 213 | c = Pacproxy::Config.instance.config 214 | c['port'] = 13_128 215 | c['pac_file']['location'] = 'spec/all_proxy.pac' 216 | @pacproxy_server = Pacproxy::Pacproxy.new(c) 217 | 218 | Thread.new { @pacproxy_server.start } 219 | wait_server_status(@pacproxy_server, :Running) 220 | 221 | c = HTTPClient.new('http://127.0.0.1:13128') 222 | begin 223 | c.get('https://127.0.0.1:13080/') 224 | rescue => e 225 | expect(e.res.status).to eq(407) 226 | expect(e.res.header['Proxy-Authenticate']) 227 | .to eq(["Basic realm=\"proxy\""]) 228 | end 229 | end 230 | end 231 | end 232 | --------------------------------------------------------------------------------