├── test ├── webrick │ ├── .htaccess │ ├── webrick.rhtml │ ├── test_config.rb │ ├── test_htgroup.rb │ ├── test_htmlutils.rb │ ├── test_httpstatus.rb │ ├── webrick_long_filename.cgi │ ├── test_httpversion.rb │ ├── webrick.cgi │ ├── test_ssl_server.rb │ ├── test_do_not_reverse_lookup.rb │ ├── utils.rb │ ├── test_utils.rb │ ├── test_https.rb │ ├── test_httputils.rb │ ├── test_cookie.rb │ ├── test_server.rb │ ├── test_cgi.rb │ └── test_httpresponse.rb └── lib │ ├── helper.rb │ ├── find_executable.rb │ └── envutil.rb ├── Gemfile ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── bin ├── setup └── console ├── lib ├── webrick │ ├── version.rb │ ├── httpservlet.rb │ ├── htmlutils.rb │ ├── compat.rb │ ├── httpservlet │ │ ├── cgi_runner.rb │ │ ├── prochandler.rb │ │ ├── erbhandler.rb │ │ ├── cgihandler.rb │ │ └── abstract.rb │ ├── httpauth │ │ ├── userdb.rb │ │ ├── htgroup.rb │ │ ├── authenticator.rb │ │ ├── basicauth.rb │ │ ├── htdigest.rb │ │ └── htpasswd.rb │ ├── httpversion.rb │ ├── httpauth.rb │ ├── https.rb │ ├── log.rb │ ├── cookie.rb │ ├── accesslog.rb │ ├── httpstatus.rb │ ├── config.rb │ ├── utils.rb │ ├── ssl.rb │ ├── cgi.rb │ ├── httpserver.rb │ ├── server.rb │ └── httpproxy.rb └── webrick.rb ├── Rakefile ├── LICENSE.txt ├── README.md └── webrick.gemspec /test/webrick/.htaccess: -------------------------------------------------------------------------------- 1 | this file should not be published. 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "test-unit" 7 | -------------------------------------------------------------------------------- /test/webrick/webrick.rhtml: -------------------------------------------------------------------------------- 1 | req to <%= 2 | servlet_request.request_uri 3 | %> <%= 4 | servlet_request.query.inspect %> 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require_relative "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | 6 | module TestWEBrick 7 | include Test::Unit::Util::Output 8 | extend Test::Unit::Util::Output 9 | end 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "webrick" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /lib/webrick/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # version.rb -- version and release date 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU 7 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: version.rb,v 1.74 2003/07/22 19:20:43 gotoyuzo Exp $ 11 | 12 | module WEBrick 13 | 14 | ## 15 | # The WEBrick version 16 | 17 | VERSION = "1.7.0" 18 | end 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :sync_tool do 11 | require 'fileutils' 12 | FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib" 13 | FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" 14 | FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" 15 | end 16 | 17 | task :default => :test 18 | -------------------------------------------------------------------------------- /test/webrick/test_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "webrick/config" 4 | 5 | class TestWEBrickConfig < Test::Unit::TestCase 6 | def test_server_name_default 7 | config = WEBrick::Config::General.dup 8 | assert_equal(false, config.key?(:ServerName)) 9 | assert_equal(WEBrick::Utils.getservername, config[:ServerName]) 10 | assert_equal(true, config.key?(:ServerName)) 11 | end 12 | 13 | def test_server_name_set_nil 14 | config = WEBrick::Config::General.dup.update(ServerName: nil) 15 | assert_equal(nil, config[:ServerName]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 8 | strategy: 9 | matrix: 10 | ruby: [ "3.0", 2.7, 2.6, 2.5, 2.4, head ] 11 | os: [ ubuntu-latest, macos-latest ] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | bundler-cache: true # 'bundle install' and cache 20 | - name: Run test 21 | run: bundle exec rake test 22 | -------------------------------------------------------------------------------- /test/lib/find_executable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "rbconfig" 3 | 4 | module EnvUtil 5 | def find_executable(cmd, *args) 6 | exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]] 7 | ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| 8 | next if path.empty? 9 | path = File.join(path, cmd) 10 | exts.each do |ext| 11 | cmdline = [path + ext, *args] 12 | begin 13 | return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read)) 14 | rescue 15 | next 16 | end 17 | end 18 | end 19 | nil 20 | end 21 | module_function :find_executable 22 | end 23 | -------------------------------------------------------------------------------- /test/webrick/test_htgroup.rb: -------------------------------------------------------------------------------- 1 | require "tempfile" 2 | require "test/unit" 3 | require "webrick/httpauth/htgroup" 4 | 5 | class TestHtgroup < Test::Unit::TestCase 6 | def test_htgroup 7 | Tempfile.create('test_htgroup') do |tmpfile| 8 | tmpfile.close 9 | tmp_group = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path) 10 | tmp_group.add 'superheroes', %w[spiderman batman] 11 | tmp_group.add 'supervillains', %w[joker] 12 | tmp_group.flush 13 | 14 | htgroup = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path) 15 | assert_equal(htgroup.members('superheroes'), %w[spiderman batman]) 16 | assert_equal(htgroup.members('supervillains'), %w[joker]) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/webrick/httpservlet.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpservlet.rb -- HTTPServlet Utility File 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: httpservlet.rb,v 1.21 2003/02/23 12:24:46 gotoyuzo Exp $ 11 | 12 | require_relative 'httpservlet/abstract' 13 | require_relative 'httpservlet/filehandler' 14 | require_relative 'httpservlet/cgihandler' 15 | require_relative 'httpservlet/erbhandler' 16 | require_relative 'httpservlet/prochandler' 17 | 18 | module WEBrick 19 | module HTTPServlet 20 | FileHandler.add_handler("cgi", CGIHandler) 21 | FileHandler.add_handler("rhtml", ERBHandler) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/webrick/htmlutils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # htmlutils.rb -- HTMLUtils Module 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: htmlutils.rb,v 1.7 2002/09/21 12:23:35 gotoyuzo Exp $ 11 | 12 | module WEBrick 13 | module HTMLUtils 14 | 15 | ## 16 | # Escapes &, ", > and < in +string+ 17 | 18 | def escape(string) 19 | return "" unless string 20 | str = string.b 21 | str.gsub!(/&/n, '&') 22 | str.gsub!(/\"/n, '"') 23 | str.gsub!(/>/n, '>') 24 | str.gsub!(/bar")) 14 | assert_equal("foo<bar", escape("foo 0 8 | res.body = p 9 | elsif (q = req.query).size > 0 10 | res.body = q.keys.sort.collect{|key| 11 | q[key].list.sort.collect{|v| 12 | "#{key}=#{v}" 13 | }.join(", ") 14 | }.join(", ") 15 | elsif %r{/$} =~ req.request_uri.to_s 16 | res.body = "" 17 | res.body << req.request_uri.to_s << "\n" 18 | res.body << req.script_name 19 | elsif !req.cookies.empty? 20 | res.body = req.cookies.inject(""){|result, cookie| 21 | result << "%s=%s\n" % [cookie.name, cookie.value] 22 | } 23 | res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE") 24 | res.cookies << WEBrick::Cookie.new("Shipping", "FedEx") 25 | else 26 | res.body = req.script_name 27 | end 28 | end 29 | 30 | def do_POST(req, res) 31 | do_GET(req, res) 32 | end 33 | end 34 | 35 | cgi = TestApp.new 36 | cgi.start 37 | -------------------------------------------------------------------------------- /test/webrick/test_httpversion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "webrick/httpversion" 4 | 5 | class TestWEBrickHTTPVersion < Test::Unit::TestCase 6 | def setup 7 | @v09 = WEBrick::HTTPVersion.new("0.9") 8 | @v10 = WEBrick::HTTPVersion.new("1.0") 9 | @v11 = WEBrick::HTTPVersion.new("1.001") 10 | end 11 | 12 | def test_to_s() 13 | assert_equal("0.9", @v09.to_s) 14 | assert_equal("1.0", @v10.to_s) 15 | assert_equal("1.1", @v11.to_s) 16 | end 17 | 18 | def test_major() 19 | assert_equal(0, @v09.major) 20 | assert_equal(1, @v10.major) 21 | assert_equal(1, @v11.major) 22 | end 23 | 24 | def test_minor() 25 | assert_equal(9, @v09.minor) 26 | assert_equal(0, @v10.minor) 27 | assert_equal(1, @v11.minor) 28 | end 29 | 30 | def test_compar() 31 | assert_equal(0, @v09 <=> "0.9") 32 | assert_equal(0, @v09 <=> "0.09") 33 | 34 | assert_equal(-1, @v09 <=> @v10) 35 | assert_equal(-1, @v09 <=> "1.00") 36 | 37 | assert_equal(1, @v11 <=> @v09) 38 | assert_equal(1, @v11 <=> "1.0") 39 | assert_equal(1, @v11 <=> "0.9") 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/webrick/webrick.cgi: -------------------------------------------------------------------------------- 1 | #!ruby 2 | require "webrick/cgi" 3 | 4 | class TestApp < WEBrick::CGI 5 | def do_GET(req, res) 6 | res["content-type"] = "text/plain" 7 | if req.path_info == "/dumpenv" 8 | res.body = Marshal.dump(ENV.to_hash) 9 | elsif (p = req.path_info) && p.length > 0 10 | res.body = p 11 | elsif (q = req.query).size > 0 12 | res.body = q.keys.sort.collect{|key| 13 | q[key].list.sort.collect{|v| 14 | "#{key}=#{v}" 15 | }.join(", ") 16 | }.join(", ") 17 | elsif %r{/$} =~ req.request_uri.to_s 18 | res.body = "" 19 | res.body << req.request_uri.to_s << "\n" 20 | res.body << req.script_name 21 | elsif !req.cookies.empty? 22 | res.body = req.cookies.inject(""){|result, cookie| 23 | result << "%s=%s\n" % [cookie.name, cookie.value] 24 | } 25 | res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE") 26 | res.cookies << WEBrick::Cookie.new("Shipping", "FedEx") 27 | else 28 | res.body = req.script_name 29 | end 30 | end 31 | 32 | def do_POST(req, res) 33 | do_GET(req, res) 34 | end 35 | end 36 | 37 | cgi = TestApp.new 38 | cgi.start 39 | -------------------------------------------------------------------------------- /lib/webrick/httpservlet/cgi_runner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # cgi_runner.rb -- CGI launcher. 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: cgi_runner.rb,v 1.9 2002/09/25 11:33:15 gotoyuzo Exp $ 11 | 12 | def sysread(io, size) 13 | buf = +"" 14 | while size > 0 15 | tmp = io.sysread(size) 16 | buf << tmp 17 | size -= tmp.bytesize 18 | end 19 | return buf 20 | end 21 | 22 | STDIN.binmode 23 | 24 | len = sysread(STDIN, 8).to_i 25 | out = sysread(STDIN, len) 26 | STDOUT.reopen(File.open(out, "w")) 27 | 28 | len = sysread(STDIN, 8).to_i 29 | err = sysread(STDIN, len) 30 | STDERR.reopen(File.open(err, "w")) 31 | 32 | len = sysread(STDIN, 8).to_i 33 | dump = sysread(STDIN, len) 34 | hash = Marshal.restore(dump) 35 | ENV.keys.each{|name| ENV.delete(name) } 36 | hash.each{|k, v| ENV[k] = v if v } 37 | 38 | dir = File::dirname(ENV["SCRIPT_FILENAME"]) 39 | Dir::chdir dir 40 | 41 | if ARGV[0] 42 | argv = ARGV.dup 43 | argv << ENV["SCRIPT_FILENAME"] 44 | exec(*argv) 45 | # NOTREACHED 46 | end 47 | exec ENV["SCRIPT_FILENAME"] 48 | -------------------------------------------------------------------------------- /lib/webrick/httpservlet/prochandler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # prochandler.rb -- ProcHandler Class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: prochandler.rb,v 1.7 2002/09/21 12:23:42 gotoyuzo Exp $ 11 | 12 | require_relative 'abstract' 13 | 14 | module WEBrick 15 | module HTTPServlet 16 | 17 | ## 18 | # Mounts a proc at a path that accepts a request and response. 19 | # 20 | # Instead of mounting this servlet with WEBrick::HTTPServer#mount use 21 | # WEBrick::HTTPServer#mount_proc: 22 | # 23 | # server.mount_proc '/' do |req, res| 24 | # res.body = 'it worked!' 25 | # res.status = 200 26 | # end 27 | 28 | class ProcHandler < AbstractServlet 29 | # :stopdoc: 30 | def get_instance(server, *options) 31 | self 32 | end 33 | 34 | def initialize(proc) 35 | @proc = proc 36 | end 37 | 38 | def do_GET(request, response) 39 | @proc.call(request, response) 40 | end 41 | 42 | alias do_POST do_GET 43 | # :startdoc: 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /lib/webrick/httpauth/userdb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # httpauth/userdb.rb -- UserDB mix-in module. 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: userdb.rb,v 1.2 2003/02/20 07:15:48 gotoyuzo Exp $ 10 | 11 | module WEBrick 12 | module HTTPAuth 13 | 14 | ## 15 | # User database mixin for HTTPAuth. This mixin dispatches user record 16 | # access to the underlying auth_type for this database. 17 | 18 | module UserDB 19 | 20 | ## 21 | # The authentication type. 22 | # 23 | # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are 24 | # built-in. 25 | 26 | attr_accessor :auth_type 27 | 28 | ## 29 | # Creates an obscured password in +realm+ with +user+ and +password+ 30 | # using the auth_type of this database. 31 | 32 | def make_passwd(realm, user, pass) 33 | @auth_type::make_passwd(realm, user, pass) 34 | end 35 | 36 | ## 37 | # Sets a password in +realm+ with +user+ and +password+ for the 38 | # auth_type of this database. 39 | 40 | def set_passwd(realm, user, pass) 41 | self[user] = pass 42 | end 43 | 44 | ## 45 | # Retrieves a password in +realm+ for +user+ for the auth_type of this 46 | # database. +reload_db+ is a dummy value. 47 | 48 | def get_passwd(realm, user, reload_db=false) 49 | make_passwd(realm, user, self[user]) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/webrick/httpversion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # HTTPVersion.rb -- presentation of HTTP version 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: httpversion.rb,v 1.5 2002/09/21 12:23:37 gotoyuzo Exp $ 10 | 11 | module WEBrick 12 | 13 | ## 14 | # Represents an HTTP protocol version 15 | 16 | class HTTPVersion 17 | include Comparable 18 | 19 | ## 20 | # The major protocol version number 21 | 22 | attr_accessor :major 23 | 24 | ## 25 | # The minor protocol version number 26 | 27 | attr_accessor :minor 28 | 29 | ## 30 | # Converts +version+ into an HTTPVersion 31 | 32 | def self.convert(version) 33 | version.is_a?(self) ? version : new(version) 34 | end 35 | 36 | ## 37 | # Creates a new HTTPVersion from +version+. 38 | 39 | def initialize(version) 40 | case version 41 | when HTTPVersion 42 | @major, @minor = version.major, version.minor 43 | when String 44 | if /^(\d+)\.(\d+)$/ =~ version 45 | @major, @minor = $1.to_i, $2.to_i 46 | end 47 | end 48 | if @major.nil? || @minor.nil? 49 | raise ArgumentError, 50 | format("cannot convert %s into %s", version.class, self.class) 51 | end 52 | end 53 | 54 | ## 55 | # Compares this version with +other+ according to the HTTP specification 56 | # rules. 57 | 58 | def <=>(other) 59 | unless other.is_a?(self.class) 60 | other = self.class.new(other) 61 | end 62 | if (ret = @major <=> other.major) == 0 63 | return @minor <=> other.minor 64 | end 65 | return ret 66 | end 67 | 68 | ## 69 | # The HTTP version as show in the HTTP request and response. For example, 70 | # "1.1" 71 | 72 | def to_s 73 | format("%d.%d", @major, @minor) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/webrick/test_ssl_server.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "webrick" 3 | require "webrick/ssl" 4 | require_relative "utils" 5 | require 'timeout' 6 | 7 | class TestWEBrickSSLServer < Test::Unit::TestCase 8 | class Echo < WEBrick::GenericServer 9 | def run(sock) 10 | while line = sock.gets 11 | sock << line 12 | end 13 | end 14 | end 15 | 16 | def test_self_signed_cert_server 17 | assert_self_signed_cert( 18 | :SSLEnable => true, 19 | :SSLCertName => [["C", "JP"], ["O", "www.ruby-lang.org"], ["CN", "Ruby"]], 20 | ) 21 | end 22 | 23 | def test_self_signed_cert_server_with_string 24 | assert_self_signed_cert( 25 | :SSLEnable => true, 26 | :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", 27 | ) 28 | end 29 | 30 | def assert_self_signed_cert(config) 31 | TestWEBrick.start_server(Echo, config){|server, addr, port, log| 32 | io = TCPSocket.new(addr, port) 33 | sock = OpenSSL::SSL::SSLSocket.new(io) 34 | sock.connect 35 | sock.puts(server.ssl_context.cert.subject.to_s) 36 | assert_equal("/C=JP/O=www.ruby-lang.org/CN=Ruby\n", sock.gets, log.call) 37 | sock.close 38 | io.close 39 | } 40 | end 41 | 42 | def test_slow_connect 43 | poke = lambda do |io, msg| 44 | begin 45 | sock = OpenSSL::SSL::SSLSocket.new(io) 46 | sock.connect 47 | sock.puts(msg) 48 | assert_equal "#{msg}\n", sock.gets, msg 49 | ensure 50 | sock&.close 51 | io.close 52 | end 53 | end 54 | config = { 55 | :SSLEnable => true, 56 | :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", 57 | } 58 | EnvUtil.timeout(10) do 59 | TestWEBrick.start_server(Echo, config) do |server, addr, port, log| 60 | outer = TCPSocket.new(addr, port) 61 | inner = TCPSocket.new(addr, port) 62 | poke.call(inner, 'fast TLS negotiation') 63 | poke.call(outer, 'slow TLS negotiation') 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webrick 2 | 3 | WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server. 4 | 5 | WEBrick features complete logging of both server operations and HTTP access. 6 | 7 | WEBrick supports both basic and digest authentication in addition to algorithms not in RFC 2617. 8 | 9 | A WEBrick server can be composed of multiple WEBrick servers or servlets to provide differing behavior on a per-host or per-path basis. WEBrick includes servlets for handling CGI scripts, ERB pages, Ruby blocks and directory listings. 10 | 11 | WEBrick also includes tools for daemonizing a process and starting a process at a higher privilege level and dropping permissions. 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | ```ruby 18 | gem 'webrick' 19 | ``` 20 | 21 | And then execute: 22 | 23 | $ bundle 24 | 25 | Or install it yourself as: 26 | 27 | $ gem install webrick 28 | 29 | ## Usage 30 | 31 | To create a new WEBrick::HTTPServer that will listen to connections on port 8000 and serve documents from the current user's public_html folder: 32 | 33 | ```ruby 34 | require 'webrick' 35 | 36 | root = File.expand_path '~/public_html' 37 | server = WEBrick::HTTPServer.new :Port => 8000, :DocumentRoot => root 38 | ``` 39 | 40 | To run the server you will need to provide a suitable shutdown hook as 41 | starting the server blocks the current thread: 42 | 43 | ```ruby 44 | trap 'INT' do server.shutdown end 45 | 46 | server.start 47 | ``` 48 | 49 | ## Development 50 | 51 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 52 | 53 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 54 | 55 | ## Contributing 56 | 57 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/webrick. 58 | 59 | ## License 60 | 61 | The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause). 62 | -------------------------------------------------------------------------------- /webrick.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | begin 3 | require_relative 'lib/webrick/version' 4 | rescue LoadError 5 | # for Ruby core repository 6 | require_relative 'version' 7 | end 8 | 9 | Gem::Specification.new do |s| 10 | s.name = "webrick" 11 | s.version = WEBrick::VERSION 12 | s.summary = "HTTP server toolkit" 13 | s.description = "WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server." 14 | 15 | s.require_path = %w{lib} 16 | s.files = [ 17 | "Gemfile", 18 | "LICENSE.txt", 19 | "README.md", 20 | "Rakefile", 21 | "lib/webrick.rb", 22 | "lib/webrick/accesslog.rb", 23 | "lib/webrick/cgi.rb", 24 | "lib/webrick/compat.rb", 25 | "lib/webrick/config.rb", 26 | "lib/webrick/cookie.rb", 27 | "lib/webrick/htmlutils.rb", 28 | "lib/webrick/httpauth.rb", 29 | "lib/webrick/httpauth/authenticator.rb", 30 | "lib/webrick/httpauth/basicauth.rb", 31 | "lib/webrick/httpauth/digestauth.rb", 32 | "lib/webrick/httpauth/htdigest.rb", 33 | "lib/webrick/httpauth/htgroup.rb", 34 | "lib/webrick/httpauth/htpasswd.rb", 35 | "lib/webrick/httpauth/userdb.rb", 36 | "lib/webrick/httpproxy.rb", 37 | "lib/webrick/httprequest.rb", 38 | "lib/webrick/httpresponse.rb", 39 | "lib/webrick/https.rb", 40 | "lib/webrick/httpserver.rb", 41 | "lib/webrick/httpservlet.rb", 42 | "lib/webrick/httpservlet/abstract.rb", 43 | "lib/webrick/httpservlet/cgi_runner.rb", 44 | "lib/webrick/httpservlet/cgihandler.rb", 45 | "lib/webrick/httpservlet/erbhandler.rb", 46 | "lib/webrick/httpservlet/filehandler.rb", 47 | "lib/webrick/httpservlet/prochandler.rb", 48 | "lib/webrick/httpstatus.rb", 49 | "lib/webrick/httputils.rb", 50 | "lib/webrick/httpversion.rb", 51 | "lib/webrick/log.rb", 52 | "lib/webrick/server.rb", 53 | "lib/webrick/ssl.rb", 54 | "lib/webrick/utils.rb", 55 | "lib/webrick/version.rb", 56 | "webrick.gemspec", 57 | ] 58 | s.required_ruby_version = ">= 2.4.0" 59 | 60 | s.authors = ["TAKAHASHI Masayoshi", "GOTOU YUUZOU", "Eric Wong"] 61 | s.email = [nil, nil, 'normal@ruby-lang.org'] 62 | s.homepage = "https://github.com/ruby/webrick" 63 | s.licenses = ["Ruby", "BSD-2-Clause"] 64 | 65 | if s.respond_to?(:metadata=) 66 | s.metadata = { 67 | "bug_tracker_uri" => "https://github.com/ruby/webrick/issues", 68 | } 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/webrick/httpservlet/erbhandler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # erbhandler.rb -- ERBHandler Class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: erbhandler.rb,v 1.25 2003/02/24 19:25:31 gotoyuzo Exp $ 11 | 12 | require_relative 'abstract' 13 | 14 | require 'erb' 15 | 16 | module WEBrick 17 | module HTTPServlet 18 | 19 | ## 20 | # ERBHandler evaluates an ERB file and returns the result. This handler 21 | # is automatically used if there are .rhtml files in a directory served by 22 | # the FileHandler. 23 | # 24 | # ERBHandler supports GET and POST methods. 25 | # 26 | # The ERB file is evaluated with the local variables +servlet_request+ and 27 | # +servlet_response+ which are a WEBrick::HTTPRequest and 28 | # WEBrick::HTTPResponse respectively. 29 | # 30 | # Example .rhtml file: 31 | # 32 | # Request to <%= servlet_request.request_uri %> 33 | # 34 | # Query params <%= servlet_request.query.inspect %> 35 | 36 | class ERBHandler < AbstractServlet 37 | 38 | ## 39 | # Creates a new ERBHandler on +server+ that will evaluate and serve the 40 | # ERB file +name+ 41 | 42 | def initialize(server, name) 43 | super(server, name) 44 | @script_filename = name 45 | end 46 | 47 | ## 48 | # Handles GET requests 49 | 50 | def do_GET(req, res) 51 | unless defined?(ERB) 52 | @logger.warn "#{self.class}: ERB not defined." 53 | raise HTTPStatus::Forbidden, "ERBHandler cannot work." 54 | end 55 | begin 56 | data = File.open(@script_filename, &:read) 57 | res.body = evaluate(ERB.new(data), req, res) 58 | res['content-type'] ||= 59 | HTTPUtils::mime_type(@script_filename, @config[:MimeTypes]) 60 | rescue StandardError 61 | raise 62 | rescue Exception => ex 63 | @logger.error(ex) 64 | raise HTTPStatus::InternalServerError, ex.message 65 | end 66 | end 67 | 68 | ## 69 | # Handles POST requests 70 | 71 | alias do_POST do_GET 72 | 73 | private 74 | 75 | ## 76 | # Evaluates +erb+ providing +servlet_request+ and +servlet_response+ as 77 | # local variables. 78 | 79 | def evaluate(erb, servlet_request, servlet_response) 80 | Module.new.module_eval{ 81 | servlet_request.meta_vars 82 | servlet_request.query 83 | erb.result(binding) 84 | } 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/webrick/httpauth/htgroup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpauth/htgroup.rb -- Apache compatible htgroup file 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: htgroup.rb,v 1.1 2003/02/16 22:22:56 gotoyuzo Exp $ 10 | 11 | require 'tempfile' 12 | 13 | module WEBrick 14 | module HTTPAuth 15 | 16 | ## 17 | # Htgroup accesses apache-compatible group files. Htgroup can be used to 18 | # provide group-based authentication for users. Currently Htgroup is not 19 | # directly integrated with any authenticators in WEBrick. For security, 20 | # the path for a digest password database should be stored outside of the 21 | # paths available to the HTTP server. 22 | # 23 | # Example: 24 | # 25 | # htgroup = WEBrick::HTTPAuth::Htgroup.new 'my_group_file' 26 | # htgroup.add 'superheroes', %w[spiderman batman] 27 | # 28 | # htgroup.members('superheroes').include? 'magneto' # => false 29 | 30 | class Htgroup 31 | 32 | ## 33 | # Open a group database at +path+ 34 | 35 | def initialize(path) 36 | @path = path 37 | @mtime = Time.at(0) 38 | @group = Hash.new 39 | File.open(@path,"a").close unless File.exist?(@path) 40 | reload 41 | end 42 | 43 | ## 44 | # Reload groups from the database 45 | 46 | def reload 47 | if (mtime = File::mtime(@path)) > @mtime 48 | @group.clear 49 | File.open(@path){|io| 50 | while line = io.gets 51 | line.chomp! 52 | group, members = line.split(/:\s*/) 53 | @group[group] = members.split(/\s+/) 54 | end 55 | } 56 | @mtime = mtime 57 | end 58 | end 59 | 60 | ## 61 | # Flush the group database. If +output+ is given the database will be 62 | # written there instead of to the original path. 63 | 64 | def flush(output=nil) 65 | output ||= @path 66 | tmp = Tempfile.create("htgroup", File::dirname(output)) 67 | begin 68 | @group.keys.sort.each{|group| 69 | tmp.puts(format("%s: %s", group, self.members(group).join(" "))) 70 | } 71 | ensure 72 | tmp.close 73 | if $! 74 | File.unlink(tmp.path) 75 | else 76 | return File.rename(tmp.path, output) 77 | end 78 | end 79 | end 80 | 81 | ## 82 | # Retrieve the list of members from +group+ 83 | 84 | def members(group) 85 | reload 86 | @group[group] || [] 87 | end 88 | 89 | ## 90 | # Add an Array of +members+ to +group+ 91 | 92 | def add(group, members) 93 | @group[group] = members(group) | members 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/webrick/test_do_not_reverse_lookup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "webrick" 4 | require_relative "utils" 5 | 6 | class TestDoNotReverseLookup < Test::Unit::TestCase 7 | class DNRL < WEBrick::GenericServer 8 | def run(sock) 9 | sock << sock.do_not_reverse_lookup.to_s 10 | end 11 | end 12 | 13 | @@original_do_not_reverse_lookup_value = Socket.do_not_reverse_lookup 14 | 15 | def teardown 16 | Socket.do_not_reverse_lookup = @@original_do_not_reverse_lookup_value 17 | end 18 | 19 | def do_not_reverse_lookup?(config) 20 | result = nil 21 | TestWEBrick.start_server(DNRL, config) do |server, addr, port, log| 22 | TCPSocket.open(addr, port) do |sock| 23 | result = {'true' => true, 'false' => false}[sock.gets] 24 | end 25 | end 26 | result 27 | end 28 | 29 | # +--------------------------------------------------------------------------+ 30 | # | Expected interaction between Socket.do_not_reverse_lookup | 31 | # | and WEBrick::Config::General[:DoNotReverseLookup] | 32 | # +----------------------------+---------------------------------------------+ 33 | # | |WEBrick::Config::General[:DoNotReverseLookup]| 34 | # +----------------------------+--------------+---------------+--------------+ 35 | # |Socket.do_not_reverse_lookup| TRUE | FALSE | NIL | 36 | # +----------------------------+--------------+---------------+--------------+ 37 | # | TRUE | true | false | true | 38 | # +----------------------------+--------------+---------------+--------------+ 39 | # | FALSE | true | false | false | 40 | # +----------------------------+--------------+---------------+--------------+ 41 | 42 | def test_socket_dnrl_true_server_dnrl_true 43 | Socket.do_not_reverse_lookup = true 44 | assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true)) 45 | end 46 | 47 | def test_socket_dnrl_true_server_dnrl_false 48 | Socket.do_not_reverse_lookup = true 49 | assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false)) 50 | end 51 | 52 | def test_socket_dnrl_true_server_dnrl_nil 53 | Socket.do_not_reverse_lookup = true 54 | assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => nil)) 55 | end 56 | 57 | def test_socket_dnrl_false_server_dnrl_true 58 | Socket.do_not_reverse_lookup = false 59 | assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true)) 60 | end 61 | 62 | def test_socket_dnrl_false_server_dnrl_false 63 | Socket.do_not_reverse_lookup = false 64 | assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false)) 65 | end 66 | 67 | def test_socket_dnrl_false_server_dnrl_nil 68 | Socket.do_not_reverse_lookup = false 69 | assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => nil)) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/webrick/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "webrick" 3 | begin 4 | require "webrick/https" 5 | rescue LoadError 6 | end 7 | require "webrick/httpproxy" 8 | 9 | module TestWEBrick 10 | NullWriter = Object.new 11 | def NullWriter.<<(msg) 12 | puts msg if $DEBUG 13 | return self 14 | end 15 | 16 | class WEBrick::HTTPServlet::CGIHandler 17 | remove_const :Ruby 18 | require "envutil" unless defined?(EnvUtil) 19 | Ruby = EnvUtil.rubybin 20 | remove_const :CGIRunner 21 | CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc: 22 | remove_const :CGIRunnerArray 23 | CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb"] # :nodoc: 24 | end 25 | 26 | RubyBin = "\"#{EnvUtil.rubybin}\"" 27 | RubyBin << " --disable-gems" 28 | RubyBin << " \"-I#{File.expand_path("../..", File.dirname(__FILE__))}/lib\"" 29 | RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/common\"" 30 | RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}\"" 31 | 32 | RubyBinArray = [EnvUtil.rubybin] 33 | RubyBinArray << "--disable-gems" 34 | RubyBinArray << "-I" << "#{File.expand_path("../..", File.dirname(__FILE__))}/lib" 35 | RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/common" 36 | RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}" 37 | 38 | require "test/unit" unless defined?(Test::Unit) 39 | include Test::Unit::Assertions 40 | extend Test::Unit::Assertions 41 | include Test::Unit::CoreAssertions 42 | extend Test::Unit::CoreAssertions 43 | 44 | module_function 45 | 46 | DefaultLogTester = lambda {|log, access_log| assert_equal([], log) } 47 | 48 | def start_server(klass, config={}, log_tester=DefaultLogTester, &block) 49 | log_ary = [] 50 | access_log_ary = [] 51 | log = proc { "webrick log start:\n" + (log_ary+access_log_ary).join.gsub(/^/, " ").chomp + "\nwebrick log end" } 52 | config = ({ 53 | :BindAddress => "127.0.0.1", :Port => 0, 54 | :ServerType => Thread, 55 | :Logger => WEBrick::Log.new(log_ary, WEBrick::BasicLog::WARN), 56 | :AccessLog => [[access_log_ary, ""]] 57 | }.update(config)) 58 | server = capture_output {break klass.new(config)} 59 | server_thread = server.start 60 | server_thread2 = Thread.new { 61 | server_thread.join 62 | if log_tester 63 | log_tester.call(log_ary, access_log_ary) 64 | end 65 | } 66 | addr = server.listeners[0].addr 67 | client_thread = Thread.new { 68 | begin 69 | block.yield([server, addr[3], addr[1], log]) 70 | ensure 71 | server.shutdown 72 | end 73 | } 74 | assert_join_threads([client_thread, server_thread2]) 75 | end 76 | 77 | def start_httpserver(config={}, log_tester=DefaultLogTester, &block) 78 | start_server(WEBrick::HTTPServer, config, log_tester, &block) 79 | end 80 | 81 | def start_httpproxy(config={}, log_tester=DefaultLogTester, &block) 82 | start_server(WEBrick::HTTPProxyServer, config, log_tester, &block) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/webrick/test_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "webrick/utils" 4 | 5 | class TestWEBrickUtils < Test::Unit::TestCase 6 | def teardown 7 | WEBrick::Utils::TimeoutHandler.terminate 8 | super 9 | end 10 | 11 | def assert_expired(m) 12 | Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do 13 | assert_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info)) 14 | end 15 | end 16 | 17 | def assert_not_expired(m) 18 | Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do 19 | assert_not_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info)) 20 | end 21 | end 22 | 23 | EX = Class.new(StandardError) 24 | 25 | def test_no_timeout 26 | m = WEBrick::Utils 27 | assert_equal(:foo, m.timeout(10){ :foo }) 28 | assert_expired(m) 29 | end 30 | 31 | def test_nested_timeout_outer 32 | m = WEBrick::Utils 33 | i = 0 34 | assert_raise(Timeout::Error){ 35 | m.timeout(1){ 36 | assert_raise(Timeout::Error){ m.timeout(0.1){ i += 1; sleep(1) } } 37 | assert_not_expired(m) 38 | i += 1 39 | sleep(2) 40 | } 41 | } 42 | assert_equal(2, i) 43 | assert_expired(m) 44 | end 45 | 46 | def test_timeout_default_exception 47 | m = WEBrick::Utils 48 | assert_raise(Timeout::Error){ m.timeout(0.01){ sleep } } 49 | assert_expired(m) 50 | end 51 | 52 | def test_timeout_custom_exception 53 | m = WEBrick::Utils 54 | ex = EX 55 | assert_raise(ex){ m.timeout(0.01, ex){ sleep } } 56 | assert_expired(m) 57 | end 58 | 59 | def test_nested_timeout_inner_custom_exception 60 | m = WEBrick::Utils 61 | ex = EX 62 | i = 0 63 | assert_raise(ex){ 64 | m.timeout(10){ 65 | m.timeout(0.01, ex){ i += 1; sleep } 66 | } 67 | sleep 68 | } 69 | assert_equal(1, i) 70 | assert_expired(m) 71 | end 72 | 73 | def test_nested_timeout_outer_custom_exception 74 | m = WEBrick::Utils 75 | ex = EX 76 | i = 0 77 | assert_raise(Timeout::Error){ 78 | m.timeout(0.01){ 79 | m.timeout(1.0, ex){ i += 1; sleep } 80 | } 81 | sleep 82 | } 83 | assert_equal(1, i) 84 | assert_expired(m) 85 | end 86 | 87 | def test_create_listeners 88 | addr = listener_address(0) 89 | port = addr.slice!(1) 90 | assert_kind_of(Integer, port, "dynamically chosen port number") 91 | assert_equal(["AF_INET", "127.0.0.1", "127.0.0.1"], addr) 92 | 93 | assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"], 94 | listener_address(port), 95 | "specific port number") 96 | 97 | assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"], 98 | listener_address(port.to_s), 99 | "specific port number string") 100 | end 101 | 102 | def listener_address(port) 103 | listeners = WEBrick::Utils.create_listeners("127.0.0.1", port) 104 | srv = listeners.first 105 | assert_kind_of TCPServer, srv 106 | srv.addr 107 | ensure 108 | listeners.each(&:close) if listeners 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/webrick/httpauth/authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # httpauth/authenticator.rb -- Authenticator mix-in module. 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: authenticator.rb,v 1.3 2003/02/20 07:15:47 gotoyuzo Exp $ 10 | 11 | module WEBrick 12 | module HTTPAuth 13 | 14 | ## 15 | # Module providing generic support for both Digest and Basic 16 | # authentication schemes. 17 | 18 | module Authenticator 19 | 20 | RequestField = "Authorization" # :nodoc: 21 | ResponseField = "WWW-Authenticate" # :nodoc: 22 | ResponseInfoField = "Authentication-Info" # :nodoc: 23 | AuthException = HTTPStatus::Unauthorized # :nodoc: 24 | 25 | ## 26 | # Method of authentication, must be overridden by the including class 27 | 28 | AuthScheme = nil 29 | 30 | ## 31 | # The realm this authenticator covers 32 | 33 | attr_reader :realm 34 | 35 | ## 36 | # The user database for this authenticator 37 | 38 | attr_reader :userdb 39 | 40 | ## 41 | # The logger for this authenticator 42 | 43 | attr_reader :logger 44 | 45 | private 46 | 47 | # :stopdoc: 48 | 49 | ## 50 | # Initializes the authenticator from +config+ 51 | 52 | def check_init(config) 53 | [:UserDB, :Realm].each{|sym| 54 | unless config[sym] 55 | raise ArgumentError, "Argument #{sym.inspect} missing." 56 | end 57 | } 58 | @realm = config[:Realm] 59 | @userdb = config[:UserDB] 60 | @logger = config[:Logger] || Log::new($stderr) 61 | @reload_db = config[:AutoReloadUserDB] 62 | @request_field = self::class::RequestField 63 | @response_field = self::class::ResponseField 64 | @resp_info_field = self::class::ResponseInfoField 65 | @auth_exception = self::class::AuthException 66 | @auth_scheme = self::class::AuthScheme 67 | end 68 | 69 | ## 70 | # Ensures +req+ has credentials that can be authenticated. 71 | 72 | def check_scheme(req) 73 | unless credentials = req[@request_field] 74 | error("no credentials in the request.") 75 | return nil 76 | end 77 | unless match = /^#{@auth_scheme}\s+/i.match(credentials) 78 | error("invalid scheme in %s.", credentials) 79 | info("%s: %s", @request_field, credentials) if $DEBUG 80 | return nil 81 | end 82 | return match.post_match 83 | end 84 | 85 | def log(meth, fmt, *args) 86 | msg = format("%s %s: ", @auth_scheme, @realm) 87 | msg << fmt % args 88 | @logger.__send__(meth, msg) 89 | end 90 | 91 | def error(fmt, *args) 92 | if @logger.error? 93 | log(:error, fmt, *args) 94 | end 95 | end 96 | 97 | def info(fmt, *args) 98 | if @logger.info? 99 | log(:info, fmt, *args) 100 | end 101 | end 102 | 103 | # :startdoc: 104 | end 105 | 106 | ## 107 | # Module providing generic support for both Digest and Basic 108 | # authentication schemes for proxies. 109 | 110 | module ProxyAuthenticator 111 | RequestField = "Proxy-Authorization" # :nodoc: 112 | ResponseField = "Proxy-Authenticate" # :nodoc: 113 | InfoField = "Proxy-Authentication-Info" # :nodoc: 114 | AuthException = HTTPStatus::ProxyAuthenticationRequired # :nodoc: 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/webrick/httpauth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpauth.rb -- HTTP access authentication 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: httpauth.rb,v 1.14 2003/07/22 19:20:42 gotoyuzo Exp $ 11 | 12 | require_relative 'httpauth/basicauth' 13 | require_relative 'httpauth/digestauth' 14 | require_relative 'httpauth/htpasswd' 15 | require_relative 'httpauth/htdigest' 16 | require_relative 'httpauth/htgroup' 17 | 18 | module WEBrick 19 | 20 | ## 21 | # HTTPAuth provides both basic and digest authentication. 22 | # 23 | # To enable authentication for requests in WEBrick you will need a user 24 | # database and an authenticator. To start, here's an Htpasswd database for 25 | # use with a DigestAuth authenticator: 26 | # 27 | # config = { :Realm => 'DigestAuth example realm' } 28 | # 29 | # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' 30 | # htpasswd.auth_type = WEBrick::HTTPAuth::DigestAuth 31 | # htpasswd.set_passwd config[:Realm], 'username', 'password' 32 | # htpasswd.flush 33 | # 34 | # The +:Realm+ is used to provide different access to different groups 35 | # across several resources on a server. Typically you'll need only one 36 | # realm for a server. 37 | # 38 | # This database can be used to create an authenticator: 39 | # 40 | # config[:UserDB] = htpasswd 41 | # 42 | # digest_auth = WEBrick::HTTPAuth::DigestAuth.new config 43 | # 44 | # To authenticate a request call #authenticate with a request and response 45 | # object in a servlet: 46 | # 47 | # def do_GET req, res 48 | # @authenticator.authenticate req, res 49 | # end 50 | # 51 | # For digest authentication the authenticator must not be created every 52 | # request, it must be passed in as an option via WEBrick::HTTPServer#mount. 53 | 54 | module HTTPAuth 55 | module_function 56 | 57 | def _basic_auth(req, res, realm, req_field, res_field, err_type, 58 | block) # :nodoc: 59 | user = pass = nil 60 | if /^Basic\s+(.*)/o =~ req[req_field] 61 | userpass = $1 62 | user, pass = userpass.unpack("m*")[0].split(":", 2) 63 | end 64 | if block.call(user, pass) 65 | req.user = user 66 | return 67 | end 68 | res[res_field] = "Basic realm=\"#{realm}\"" 69 | raise err_type 70 | end 71 | 72 | ## 73 | # Simple wrapper for providing basic authentication for a request. When 74 | # called with a request +req+, response +res+, authentication +realm+ and 75 | # +block+ the block will be called with a +username+ and +password+. If 76 | # the block returns true the request is allowed to continue, otherwise an 77 | # HTTPStatus::Unauthorized error is raised. 78 | 79 | def basic_auth(req, res, realm, &block) # :yield: username, password 80 | _basic_auth(req, res, realm, "Authorization", "WWW-Authenticate", 81 | HTTPStatus::Unauthorized, block) 82 | end 83 | 84 | ## 85 | # Simple wrapper for providing basic authentication for a proxied request. 86 | # When called with a request +req+, response +res+, authentication +realm+ 87 | # and +block+ the block will be called with a +username+ and +password+. 88 | # If the block returns true the request is allowed to continue, otherwise 89 | # an HTTPStatus::ProxyAuthenticationRequired error is raised. 90 | 91 | def proxy_basic_auth(req, res, realm, &block) # :yield: username, password 92 | _basic_auth(req, res, realm, "Proxy-Authorization", "Proxy-Authenticate", 93 | HTTPStatus::ProxyAuthenticationRequired, block) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/webrick/httpauth/basicauth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpauth/basicauth.rb -- HTTP basic access authentication 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: basicauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ 10 | 11 | require_relative '../config' 12 | require_relative '../httpstatus' 13 | require_relative 'authenticator' 14 | 15 | module WEBrick 16 | module HTTPAuth 17 | 18 | ## 19 | # Basic Authentication for WEBrick 20 | # 21 | # Use this class to add basic authentication to a WEBrick servlet. 22 | # 23 | # Here is an example of how to set up a BasicAuth: 24 | # 25 | # config = { :Realm => 'BasicAuth example realm' } 26 | # 27 | # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file', password_hash: :bcrypt 28 | # htpasswd.set_passwd config[:Realm], 'username', 'password' 29 | # htpasswd.flush 30 | # 31 | # config[:UserDB] = htpasswd 32 | # 33 | # basic_auth = WEBrick::HTTPAuth::BasicAuth.new config 34 | 35 | class BasicAuth 36 | include Authenticator 37 | 38 | AuthScheme = "Basic" # :nodoc: 39 | 40 | ## 41 | # Used by UserDB to create a basic password entry 42 | 43 | def self.make_passwd(realm, user, pass) 44 | pass ||= "" 45 | pass.crypt(Utils::random_string(2)) 46 | end 47 | 48 | attr_reader :realm, :userdb, :logger 49 | 50 | ## 51 | # Creates a new BasicAuth instance. 52 | # 53 | # See WEBrick::Config::BasicAuth for default configuration entries 54 | # 55 | # You must supply the following configuration entries: 56 | # 57 | # :Realm:: The name of the realm being protected. 58 | # :UserDB:: A database of usernames and passwords. 59 | # A WEBrick::HTTPAuth::Htpasswd instance should be used. 60 | 61 | def initialize(config, default=Config::BasicAuth) 62 | check_init(config) 63 | @config = default.dup.update(config) 64 | end 65 | 66 | ## 67 | # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if 68 | # the authentication was not correct. 69 | 70 | def authenticate(req, res) 71 | unless basic_credentials = check_scheme(req) 72 | challenge(req, res) 73 | end 74 | userid, password = basic_credentials.unpack("m*")[0].split(":", 2) 75 | password ||= "" 76 | if userid.empty? 77 | error("user id was not given.") 78 | challenge(req, res) 79 | end 80 | unless encpass = @userdb.get_passwd(@realm, userid, @reload_db) 81 | error("%s: the user is not allowed.", userid) 82 | challenge(req, res) 83 | end 84 | 85 | case encpass 86 | when /\A\$2[aby]\$/ 87 | password_matches = BCrypt::Password.new(encpass.sub(/\A\$2[aby]\$/, '$2a$')) == password 88 | else 89 | password_matches = password.crypt(encpass) == encpass 90 | end 91 | 92 | unless password_matches 93 | error("%s: password unmatch.", userid) 94 | challenge(req, res) 95 | end 96 | info("%s: authentication succeeded.", userid) 97 | req.user = userid 98 | end 99 | 100 | ## 101 | # Returns a challenge response which asks for authentication information 102 | 103 | def challenge(req, res) 104 | res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\"" 105 | raise @auth_exception 106 | end 107 | end 108 | 109 | ## 110 | # Basic authentication for proxy servers. See BasicAuth for details. 111 | 112 | class ProxyBasicAuth < BasicAuth 113 | include ProxyAuthenticator 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/webrick/test_https.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "net/http" 4 | require "webrick" 5 | require "webrick/https" 6 | require "webrick/utils" 7 | require_relative "utils" 8 | 9 | class TestWEBrickHTTPS < Test::Unit::TestCase 10 | empty_log = Object.new 11 | def empty_log.<<(str) 12 | assert_equal('', str) 13 | self 14 | end 15 | NoLog = WEBrick::Log.new(empty_log, WEBrick::BasicLog::WARN) 16 | 17 | class HTTPSNITest < ::Net::HTTP 18 | attr_accessor :sni_hostname 19 | 20 | def ssl_socket_connect(s, timeout) 21 | s.hostname = sni_hostname 22 | super 23 | end 24 | end 25 | 26 | def teardown 27 | WEBrick::Utils::TimeoutHandler.terminate 28 | super 29 | end 30 | 31 | def https_get(addr, port, hostname, path, verifyname = nil) 32 | subject = nil 33 | http = HTTPSNITest.new(addr, port) 34 | http.use_ssl = true 35 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 36 | http.verify_callback = proc { |x, store| subject = store.chain[0].subject.to_s; x } 37 | http.sni_hostname = hostname 38 | req = Net::HTTP::Get.new(path) 39 | req["Host"] = "#{hostname}:#{port}" 40 | response = http.start { http.request(req).body } 41 | assert_equal("/CN=#{verifyname || hostname}", subject) 42 | response 43 | end 44 | 45 | def test_sni 46 | config = { 47 | :ServerName => "localhost", 48 | :SSLEnable => true, 49 | :SSLCertName => "/CN=localhost", 50 | } 51 | TestWEBrick.start_httpserver(config){|server, addr, port, log| 52 | server.mount_proc("/") {|req, res| res.body = "master" } 53 | 54 | # catch stderr in create_self_signed_cert 55 | stderr_buffer = StringIO.new 56 | old_stderr, $stderr = $stderr, stderr_buffer 57 | 58 | begin 59 | vhost_config1 = { 60 | :ServerName => "vhost1", 61 | :Port => port, 62 | :DoNotListen => true, 63 | :Logger => NoLog, 64 | :AccessLog => [], 65 | :SSLEnable => true, 66 | :SSLCertName => "/CN=vhost1", 67 | } 68 | vhost1 = WEBrick::HTTPServer.new(vhost_config1) 69 | vhost1.mount_proc("/") {|req, res| res.body = "vhost1" } 70 | server.virtual_host(vhost1) 71 | 72 | vhost_config2 = { 73 | :ServerName => "vhost2", 74 | :ServerAlias => ["vhost2alias"], 75 | :Port => port, 76 | :DoNotListen => true, 77 | :Logger => NoLog, 78 | :AccessLog => [], 79 | :SSLEnable => true, 80 | :SSLCertName => "/CN=vhost2", 81 | } 82 | vhost2 = WEBrick::HTTPServer.new(vhost_config2) 83 | vhost2.mount_proc("/") {|req, res| res.body = "vhost2" } 84 | server.virtual_host(vhost2) 85 | ensure 86 | # restore stderr 87 | $stderr = old_stderr 88 | end 89 | 90 | assert_match(/\A([.+*]+\n)+\z/, stderr_buffer.string) 91 | assert_equal("master", https_get(addr, port, "localhost", "/localhost")) 92 | assert_equal("master", https_get(addr, port, "unknown", "/unknown", "localhost")) 93 | assert_equal("vhost1", https_get(addr, port, "vhost1", "/vhost1")) 94 | assert_equal("vhost2", https_get(addr, port, "vhost2", "/vhost2")) 95 | assert_equal("vhost2", https_get(addr, port, "vhost2alias", "/vhost2alias", "vhost2")) 96 | } 97 | end 98 | 99 | def test_check_ssl_virtual 100 | config = { 101 | :ServerName => "localhost", 102 | :SSLEnable => true, 103 | :SSLCertName => "/CN=localhost", 104 | } 105 | TestWEBrick.start_httpserver(config){|server, addr, port, log| 106 | assert_raise ArgumentError do 107 | vhost = WEBrick::HTTPServer.new({:DoNotListen => true, :Logger => NoLog}) 108 | server.virtual_host(vhost) 109 | end 110 | } 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/webrick/https.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # https.rb -- SSL/TLS enhancement for HTTPServer 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2001 GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: https.rb,v 1.15 2003/07/22 19:20:42 gotoyuzo Exp $ 11 | 12 | require_relative 'ssl' 13 | require_relative 'httpserver' 14 | 15 | module WEBrick 16 | module Config 17 | HTTP.update(SSL) 18 | end 19 | 20 | ## 21 | #-- 22 | # Adds SSL functionality to WEBrick::HTTPRequest 23 | 24 | class HTTPRequest 25 | 26 | ## 27 | # HTTP request SSL cipher 28 | 29 | attr_reader :cipher 30 | 31 | ## 32 | # HTTP request server certificate 33 | 34 | attr_reader :server_cert 35 | 36 | ## 37 | # HTTP request client certificate 38 | 39 | attr_reader :client_cert 40 | 41 | # :stopdoc: 42 | 43 | alias orig_parse parse 44 | 45 | def parse(socket=nil) 46 | if socket.respond_to?(:cert) 47 | @server_cert = socket.cert || @config[:SSLCertificate] 48 | @client_cert = socket.peer_cert 49 | @client_cert_chain = socket.peer_cert_chain 50 | @cipher = socket.cipher 51 | end 52 | orig_parse(socket) 53 | end 54 | 55 | alias orig_parse_uri parse_uri 56 | 57 | def parse_uri(str, scheme="https") 58 | if server_cert 59 | return orig_parse_uri(str, scheme) 60 | end 61 | return orig_parse_uri(str) 62 | end 63 | private :parse_uri 64 | 65 | alias orig_meta_vars meta_vars 66 | 67 | def meta_vars 68 | meta = orig_meta_vars 69 | if server_cert 70 | meta["HTTPS"] = "on" 71 | meta["SSL_SERVER_CERT"] = @server_cert.to_pem 72 | meta["SSL_CLIENT_CERT"] = @client_cert ? @client_cert.to_pem : "" 73 | if @client_cert_chain 74 | @client_cert_chain.each_with_index{|cert, i| 75 | meta["SSL_CLIENT_CERT_CHAIN_#{i}"] = cert.to_pem 76 | } 77 | end 78 | meta["SSL_CIPHER"] = @cipher[0] 79 | meta["SSL_PROTOCOL"] = @cipher[1] 80 | meta["SSL_CIPHER_USEKEYSIZE"] = @cipher[2].to_s 81 | meta["SSL_CIPHER_ALGKEYSIZE"] = @cipher[3].to_s 82 | end 83 | meta 84 | end 85 | 86 | # :startdoc: 87 | end 88 | 89 | ## 90 | #-- 91 | # Fake WEBrick::HTTPRequest for lookup_server 92 | 93 | class SNIRequest 94 | 95 | ## 96 | # The SNI hostname 97 | 98 | attr_reader :host 99 | 100 | ## 101 | # The socket address of the server 102 | 103 | attr_reader :addr 104 | 105 | ## 106 | # The port this request is for 107 | 108 | attr_reader :port 109 | 110 | ## 111 | # Creates a new SNIRequest. 112 | 113 | def initialize(sslsocket, hostname) 114 | @host = hostname 115 | @addr = sslsocket.addr 116 | @port = @addr[1] 117 | end 118 | end 119 | 120 | 121 | ## 122 | #-- 123 | # Adds SSL functionality to WEBrick::HTTPServer 124 | 125 | class HTTPServer < ::WEBrick::GenericServer 126 | ## 127 | # ServerNameIndication callback 128 | 129 | def ssl_servername_callback(sslsocket, hostname = nil) 130 | req = SNIRequest.new(sslsocket, hostname) 131 | server = lookup_server(req) 132 | server ? server.ssl_context : nil 133 | end 134 | 135 | # :stopdoc: 136 | 137 | ## 138 | # Check whether +server+ is also SSL server. 139 | # Also +server+'s SSL context will be created. 140 | 141 | alias orig_virtual_host virtual_host 142 | 143 | def virtual_host(server) 144 | if @config[:SSLEnable] && !server.ssl_context 145 | raise ArgumentError, "virtual host must set SSLEnable to true" 146 | end 147 | orig_virtual_host(server) 148 | end 149 | 150 | # :startdoc: 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/webrick/httpauth/htdigest.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpauth/htdigest.rb -- Apache compatible htdigest file 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: htdigest.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ 10 | 11 | require_relative 'userdb' 12 | require_relative 'digestauth' 13 | require 'tempfile' 14 | 15 | module WEBrick 16 | module HTTPAuth 17 | 18 | ## 19 | # Htdigest accesses apache-compatible digest password files. Passwords are 20 | # matched to a realm where they are valid. For security, the path for a 21 | # digest password database should be stored outside of the paths available 22 | # to the HTTP server. 23 | # 24 | # Htdigest is intended for use with WEBrick::HTTPAuth::DigestAuth and 25 | # stores passwords using cryptographic hashes. 26 | # 27 | # htpasswd = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' 28 | # htpasswd.set_passwd 'my realm', 'username', 'password' 29 | # htpasswd.flush 30 | 31 | class Htdigest 32 | include UserDB 33 | 34 | ## 35 | # Open a digest password database at +path+ 36 | 37 | def initialize(path) 38 | @path = path 39 | @mtime = Time.at(0) 40 | @digest = Hash.new 41 | @mutex = Thread::Mutex::new 42 | @auth_type = DigestAuth 43 | File.open(@path,"a").close unless File.exist?(@path) 44 | reload 45 | end 46 | 47 | ## 48 | # Reloads passwords from the database 49 | 50 | def reload 51 | mtime = File::mtime(@path) 52 | if mtime > @mtime 53 | @digest.clear 54 | File.open(@path){|io| 55 | while line = io.gets 56 | line.chomp! 57 | user, realm, pass = line.split(/:/, 3) 58 | unless @digest[realm] 59 | @digest[realm] = Hash.new 60 | end 61 | @digest[realm][user] = pass 62 | end 63 | } 64 | @mtime = mtime 65 | end 66 | end 67 | 68 | ## 69 | # Flush the password database. If +output+ is given the database will 70 | # be written there instead of to the original path. 71 | 72 | def flush(output=nil) 73 | output ||= @path 74 | tmp = Tempfile.create("htpasswd", File::dirname(output)) 75 | renamed = false 76 | begin 77 | each{|item| tmp.puts(item.join(":")) } 78 | tmp.close 79 | File::rename(tmp.path, output) 80 | renamed = true 81 | ensure 82 | tmp.close 83 | File.unlink(tmp.path) if !renamed 84 | end 85 | end 86 | 87 | ## 88 | # Retrieves a password from the database for +user+ in +realm+. If 89 | # +reload_db+ is true the database will be reloaded first. 90 | 91 | def get_passwd(realm, user, reload_db) 92 | reload() if reload_db 93 | if hash = @digest[realm] 94 | hash[user] 95 | end 96 | end 97 | 98 | ## 99 | # Sets a password in the database for +user+ in +realm+ to +pass+. 100 | 101 | def set_passwd(realm, user, pass) 102 | @mutex.synchronize{ 103 | unless @digest[realm] 104 | @digest[realm] = Hash.new 105 | end 106 | @digest[realm][user] = make_passwd(realm, user, pass) 107 | } 108 | end 109 | 110 | ## 111 | # Removes a password from the database for +user+ in +realm+. 112 | 113 | def delete_passwd(realm, user) 114 | if hash = @digest[realm] 115 | hash.delete(user) 116 | end 117 | end 118 | 119 | ## 120 | # Iterate passwords in the database. 121 | 122 | def each # :yields: [user, realm, password_hash] 123 | @digest.keys.sort.each{|realm| 124 | hash = @digest[realm] 125 | hash.keys.sort.each{|user| 126 | yield([user, realm, hash[user]]) 127 | } 128 | } 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/webrick/httpservlet/cgihandler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # cgihandler.rb -- CGIHandler Class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: cgihandler.rb,v 1.27 2003/03/21 19:56:01 gotoyuzo Exp $ 11 | 12 | require 'rbconfig' 13 | require 'tempfile' 14 | require_relative '../config' 15 | require_relative 'abstract' 16 | 17 | module WEBrick 18 | module HTTPServlet 19 | 20 | ## 21 | # Servlet for handling CGI scripts 22 | # 23 | # Example: 24 | # 25 | # server.mount('/cgi/my_script', WEBrick::HTTPServlet::CGIHandler, 26 | # '/path/to/my_script') 27 | 28 | class CGIHandler < AbstractServlet 29 | Ruby = RbConfig.ruby # :nodoc: 30 | CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc: 31 | CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb".freeze].freeze # :nodoc: 32 | 33 | ## 34 | # Creates a new CGI script servlet for the script at +name+ 35 | 36 | def initialize(server, name) 37 | super(server, name) 38 | @script_filename = name 39 | @tempdir = server[:TempDir] 40 | interpreter = server[:CGIInterpreter] 41 | if interpreter.is_a?(Array) 42 | @cgicmd = CGIRunnerArray + interpreter 43 | else 44 | @cgicmd = "#{CGIRunner} #{interpreter}" 45 | end 46 | end 47 | 48 | # :stopdoc: 49 | 50 | def do_GET(req, res) 51 | cgi_in = IO::popen(@cgicmd, "wb") 52 | cgi_out = Tempfile.new("webrick.cgiout.", @tempdir, mode: IO::BINARY) 53 | cgi_out.set_encoding("ASCII-8BIT") 54 | cgi_err = Tempfile.new("webrick.cgierr.", @tempdir, mode: IO::BINARY) 55 | cgi_err.set_encoding("ASCII-8BIT") 56 | begin 57 | cgi_in.sync = true 58 | meta = req.meta_vars 59 | meta["SCRIPT_FILENAME"] = @script_filename 60 | meta["PATH"] = @config[:CGIPathEnv] 61 | meta.delete("HTTP_PROXY") 62 | if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM 63 | meta["SystemRoot"] = ENV["SystemRoot"] 64 | end 65 | dump = Marshal.dump(meta) 66 | 67 | cgi_in.write("%8d" % cgi_out.path.bytesize) 68 | cgi_in.write(cgi_out.path) 69 | cgi_in.write("%8d" % cgi_err.path.bytesize) 70 | cgi_in.write(cgi_err.path) 71 | cgi_in.write("%8d" % dump.bytesize) 72 | cgi_in.write(dump) 73 | 74 | req.body { |chunk| cgi_in.write(chunk) } 75 | ensure 76 | cgi_in.close 77 | status = $?.exitstatus 78 | sleep 0.1 if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM 79 | data = cgi_out.read 80 | cgi_out.close(true) 81 | if errmsg = cgi_err.read 82 | if errmsg.bytesize > 0 83 | @logger.error("CGIHandler: #{@script_filename}:\n" + errmsg) 84 | end 85 | end 86 | cgi_err.close(true) 87 | end 88 | 89 | if status != 0 90 | @logger.error("CGIHandler: #{@script_filename} exit with #{status}") 91 | end 92 | 93 | data = "" unless data 94 | raw_header, body = data.split(/^[\xd\xa]+/, 2) 95 | raise HTTPStatus::InternalServerError, 96 | "Premature end of script headers: #{@script_filename}" if body.nil? 97 | 98 | begin 99 | header = HTTPUtils::parse_header(raw_header) 100 | if /^(\d+)/ =~ header['status'][0] 101 | res.status = $1.to_i 102 | header.delete('status') 103 | end 104 | if header.has_key?('location') 105 | # RFC 3875 6.2.3, 6.2.4 106 | res.status = 302 unless (300...400) === res.status 107 | end 108 | if header.has_key?('set-cookie') 109 | header['set-cookie'].each{|k| 110 | res.cookies << Cookie.parse_set_cookie(k) 111 | } 112 | header.delete('set-cookie') 113 | end 114 | header.each{|key, val| res[key] = val.join(", ") } 115 | rescue => ex 116 | raise HTTPStatus::InternalServerError, ex.message 117 | end 118 | res.body = body 119 | end 120 | alias do_POST do_GET 121 | 122 | # :startdoc: 123 | end 124 | 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/webrick/log.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # log.rb -- Log Class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: log.rb,v 1.26 2002/10/06 17:06:10 gotoyuzo Exp $ 11 | 12 | module WEBrick 13 | 14 | ## 15 | # A generic logging class 16 | 17 | class BasicLog 18 | 19 | # Fatal log level which indicates a server crash 20 | 21 | FATAL = 1 22 | 23 | # Error log level which indicates a recoverable error 24 | 25 | ERROR = 2 26 | 27 | # Warning log level which indicates a possible problem 28 | 29 | WARN = 3 30 | 31 | # Information log level which indicates possibly useful information 32 | 33 | INFO = 4 34 | 35 | # Debugging error level for messages used in server development or 36 | # debugging 37 | 38 | DEBUG = 5 39 | 40 | # log-level, messages above this level will be logged 41 | attr_accessor :level 42 | 43 | ## 44 | # Initializes a new logger for +log_file+ that outputs messages at +level+ 45 | # or higher. +log_file+ can be a filename, an IO-like object that 46 | # responds to #<< or nil which outputs to $stderr. 47 | # 48 | # If no level is given INFO is chosen by default 49 | 50 | def initialize(log_file=nil, level=nil) 51 | @level = level || INFO 52 | case log_file 53 | when String 54 | @log = File.open(log_file, "a+") 55 | @log.sync = true 56 | @opened = true 57 | when NilClass 58 | @log = $stderr 59 | else 60 | @log = log_file # requires "<<". (see BasicLog#log) 61 | end 62 | end 63 | 64 | ## 65 | # Closes the logger (also closes the log device associated to the logger) 66 | def close 67 | @log.close if @opened 68 | @log = nil 69 | end 70 | 71 | ## 72 | # Logs +data+ at +level+ if the given level is above the current log 73 | # level. 74 | 75 | def log(level, data) 76 | if @log && level <= @level 77 | data += "\n" if /\n\Z/ !~ data 78 | @log << data 79 | end 80 | end 81 | 82 | ## 83 | # Synonym for log(INFO, obj.to_s) 84 | def <<(obj) 85 | log(INFO, obj.to_s) 86 | end 87 | 88 | # Shortcut for logging a FATAL message 89 | def fatal(msg) log(FATAL, "FATAL " + format(msg)); end 90 | # Shortcut for logging an ERROR message 91 | def error(msg) log(ERROR, "ERROR " + format(msg)); end 92 | # Shortcut for logging a WARN message 93 | def warn(msg) log(WARN, "WARN " + format(msg)); end 94 | # Shortcut for logging an INFO message 95 | def info(msg) log(INFO, "INFO " + format(msg)); end 96 | # Shortcut for logging a DEBUG message 97 | def debug(msg) log(DEBUG, "DEBUG " + format(msg)); end 98 | 99 | # Will the logger output FATAL messages? 100 | def fatal?; @level >= FATAL; end 101 | # Will the logger output ERROR messages? 102 | def error?; @level >= ERROR; end 103 | # Will the logger output WARN messages? 104 | def warn?; @level >= WARN; end 105 | # Will the logger output INFO messages? 106 | def info?; @level >= INFO; end 107 | # Will the logger output DEBUG messages? 108 | def debug?; @level >= DEBUG; end 109 | 110 | private 111 | 112 | ## 113 | # Formats +arg+ for the logger 114 | # 115 | # * If +arg+ is an Exception, it will format the error message and 116 | # the back trace. 117 | # * If +arg+ responds to #to_str, it will return it. 118 | # * Otherwise it will return +arg+.inspect. 119 | def format(arg) 120 | if arg.is_a?(Exception) 121 | +"#{arg.class}: #{AccessLog.escape(arg.message)}\n\t" << 122 | arg.backtrace.join("\n\t") << "\n" 123 | elsif arg.respond_to?(:to_str) 124 | AccessLog.escape(arg.to_str) 125 | else 126 | arg.inspect 127 | end 128 | end 129 | end 130 | 131 | ## 132 | # A logging class that prepends a timestamp to each message. 133 | 134 | class Log < BasicLog 135 | # Format of the timestamp which is applied to each logged line. The 136 | # default is "[%Y-%m-%d %H:%M:%S]" 137 | attr_accessor :time_format 138 | 139 | ## 140 | # Same as BasicLog#initialize 141 | # 142 | # You can set the timestamp format through #time_format 143 | def initialize(log_file=nil, level=nil) 144 | super(log_file, level) 145 | @time_format = "[%Y-%m-%d %H:%M:%S]" 146 | end 147 | 148 | ## 149 | # Same as BasicLog#log 150 | def log(level, data) 151 | tmp = Time.now.strftime(@time_format) 152 | tmp << " " << data 153 | super(level, tmp) 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /lib/webrick/cookie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # cookie.rb -- Cookie class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: cookie.rb,v 1.16 2002/09/21 12:23:35 gotoyuzo Exp $ 11 | 12 | require 'time' 13 | require_relative 'httputils' 14 | 15 | module WEBrick 16 | 17 | ## 18 | # Processes HTTP cookies 19 | 20 | class Cookie 21 | 22 | ## 23 | # The cookie name 24 | 25 | attr_reader :name 26 | 27 | ## 28 | # The cookie value 29 | 30 | attr_accessor :value 31 | 32 | ## 33 | # The cookie version 34 | 35 | attr_accessor :version 36 | 37 | ## 38 | # The cookie domain 39 | attr_accessor :domain 40 | 41 | ## 42 | # The cookie path 43 | 44 | attr_accessor :path 45 | 46 | ## 47 | # Is this a secure cookie? 48 | 49 | attr_accessor :secure 50 | 51 | ## 52 | # The cookie comment 53 | 54 | attr_accessor :comment 55 | 56 | ## 57 | # The maximum age of the cookie 58 | 59 | attr_accessor :max_age 60 | 61 | #attr_accessor :comment_url, :discard, :port 62 | 63 | ## 64 | # Creates a new cookie with the given +name+ and +value+ 65 | 66 | def initialize(name, value) 67 | @name = name 68 | @value = value 69 | @version = 0 # Netscape Cookie 70 | 71 | @domain = @path = @secure = @comment = @max_age = 72 | @expires = @comment_url = @discard = @port = nil 73 | end 74 | 75 | ## 76 | # Sets the cookie expiration to the time +t+. The expiration time may be 77 | # a false value to disable expiration or a Time or HTTP format time string 78 | # to set the expiration date. 79 | 80 | def expires=(t) 81 | @expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s) 82 | end 83 | 84 | ## 85 | # Retrieves the expiration time as a Time 86 | 87 | def expires 88 | @expires && Time.parse(@expires) 89 | end 90 | 91 | ## 92 | # The cookie string suitable for use in an HTTP header 93 | 94 | def to_s 95 | ret = +"" 96 | ret << @name << "=" << @value 97 | ret << "; " << "Version=" << @version.to_s if @version > 0 98 | ret << "; " << "Domain=" << @domain if @domain 99 | ret << "; " << "Expires=" << @expires if @expires 100 | ret << "; " << "Max-Age=" << @max_age.to_s if @max_age 101 | ret << "; " << "Comment=" << @comment if @comment 102 | ret << "; " << "Path=" << @path if @path 103 | ret << "; " << "Secure" if @secure 104 | ret 105 | end 106 | 107 | ## 108 | # Parses a Cookie field sent from the user-agent. Returns an array of 109 | # cookies. 110 | 111 | def self.parse(str) 112 | if str 113 | ret = [] 114 | cookie = nil 115 | ver = 0 116 | str.split(/;\s+/).each{|x| 117 | key, val = x.split(/=/,2) 118 | val = val ? HTTPUtils::dequote(val) : "" 119 | case key 120 | when "$Version"; ver = val.to_i 121 | when "$Path"; cookie.path = val 122 | when "$Domain"; cookie.domain = val 123 | when "$Port"; cookie.port = val 124 | else 125 | ret << cookie if cookie 126 | cookie = self.new(key, val) 127 | cookie.version = ver 128 | end 129 | } 130 | ret << cookie if cookie 131 | ret 132 | end 133 | end 134 | 135 | ## 136 | # Parses the cookie in +str+ 137 | 138 | def self.parse_set_cookie(str) 139 | cookie_elem = str.split(/;/) 140 | first_elem = cookie_elem.shift 141 | first_elem.strip! 142 | key, value = first_elem.split(/=/, 2) 143 | cookie = new(key, HTTPUtils.dequote(value)) 144 | cookie_elem.each{|pair| 145 | pair.strip! 146 | key, value = pair.split(/=/, 2) 147 | if value 148 | value = HTTPUtils.dequote(value.strip) 149 | end 150 | case key.downcase 151 | when "domain" then cookie.domain = value 152 | when "path" then cookie.path = value 153 | when "expires" then cookie.expires = value 154 | when "max-age" then cookie.max_age = Integer(value) 155 | when "comment" then cookie.comment = value 156 | when "version" then cookie.version = Integer(value) 157 | when "secure" then cookie.secure = true 158 | end 159 | } 160 | return cookie 161 | end 162 | 163 | ## 164 | # Parses the cookies in +str+ 165 | 166 | def self.parse_set_cookies(str) 167 | return str.split(/,(?=[^;,]*=)|,$/).collect{|c| 168 | parse_set_cookie(c) 169 | } 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/webrick/httpservlet/abstract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpservlet.rb -- HTTPServlet Module 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: abstract.rb,v 1.24 2003/07/11 11:16:46 gotoyuzo Exp $ 11 | 12 | require_relative '../htmlutils' 13 | require_relative '../httputils' 14 | require_relative '../httpstatus' 15 | 16 | module WEBrick 17 | module HTTPServlet 18 | class HTTPServletError < StandardError; end 19 | 20 | ## 21 | # AbstractServlet allows HTTP server modules to be reused across multiple 22 | # servers and allows encapsulation of functionality. 23 | # 24 | # By default a servlet will respond to GET, HEAD (through an alias to GET) 25 | # and OPTIONS requests. 26 | # 27 | # By default a new servlet is initialized for every request. A servlet 28 | # instance can be reused by overriding ::get_instance in the 29 | # AbstractServlet subclass. 30 | # 31 | # == A Simple Servlet 32 | # 33 | # class Simple < WEBrick::HTTPServlet::AbstractServlet 34 | # def do_GET request, response 35 | # status, content_type, body = do_stuff_with request 36 | # 37 | # response.status = status 38 | # response['Content-Type'] = content_type 39 | # response.body = body 40 | # end 41 | # 42 | # def do_stuff_with request 43 | # return 200, 'text/plain', 'you got a page' 44 | # end 45 | # end 46 | # 47 | # This servlet can be mounted on a server at a given path: 48 | # 49 | # server.mount '/simple', Simple 50 | # 51 | # == Servlet Configuration 52 | # 53 | # Servlets can be configured via initialize. The first argument is the 54 | # HTTP server the servlet is being initialized for. 55 | # 56 | # class Configurable < Simple 57 | # def initialize server, color, size 58 | # super server 59 | # @color = color 60 | # @size = size 61 | # end 62 | # 63 | # def do_stuff_with request 64 | # content = "

Hello, World!" 67 | # 68 | # return 200, "text/html", content 69 | # end 70 | # end 71 | # 72 | # This servlet must be provided two arguments at mount time: 73 | # 74 | # server.mount '/configurable', Configurable, 'red', '2em' 75 | 76 | class AbstractServlet 77 | 78 | ## 79 | # Factory for servlet instances that will handle a request from +server+ 80 | # using +options+ from the mount point. By default a new servlet 81 | # instance is created for every call. 82 | 83 | def self.get_instance(server, *options) 84 | self.new(server, *options) 85 | end 86 | 87 | ## 88 | # Initializes a new servlet for +server+ using +options+ which are 89 | # stored as-is in +@options+. +@logger+ is also provided. 90 | 91 | def initialize(server, *options) 92 | @server = @config = server 93 | @logger = @server[:Logger] 94 | @options = options 95 | end 96 | 97 | ## 98 | # Dispatches to a +do_+ method based on +req+ if such a method is 99 | # available. (+do_GET+ for a GET request). Raises a MethodNotAllowed 100 | # exception if the method is not implemented. 101 | 102 | def service(req, res) 103 | method_name = "do_" + req.request_method.gsub(/-/, "_") 104 | if respond_to?(method_name) 105 | __send__(method_name, req, res) 106 | else 107 | raise HTTPStatus::MethodNotAllowed, 108 | "unsupported method `#{req.request_method}'." 109 | end 110 | end 111 | 112 | ## 113 | # Raises a NotFound exception 114 | 115 | def do_GET(req, res) 116 | raise HTTPStatus::NotFound, "not found." 117 | end 118 | 119 | ## 120 | # Dispatches to do_GET 121 | 122 | def do_HEAD(req, res) 123 | do_GET(req, res) 124 | end 125 | 126 | ## 127 | # Returns the allowed HTTP request methods 128 | 129 | def do_OPTIONS(req, res) 130 | m = self.methods.grep(/\Ado_([A-Z]+)\z/) {$1} 131 | m.sort! 132 | res["allow"] = m.join(",") 133 | end 134 | 135 | private 136 | 137 | ## 138 | # Redirects to a path ending in / 139 | 140 | def redirect_to_directory_uri(req, res) 141 | if req.path[-1] != ?/ 142 | location = WEBrick::HTTPUtils.escape_path(req.path + "/") 143 | if req.query_string && req.query_string.bytesize > 0 144 | location << "?" << req.query_string 145 | end 146 | res.set_redirect(HTTPStatus::MovedPermanently, location) 147 | end 148 | end 149 | end 150 | 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /test/webrick/test_httputils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "webrick/httputils" 4 | 5 | class TestWEBrickHTTPUtils < Test::Unit::TestCase 6 | include WEBrick::HTTPUtils 7 | 8 | def test_normilize_path 9 | assert_equal("/foo", normalize_path("/foo")) 10 | assert_equal("/foo/bar/", normalize_path("/foo/bar/")) 11 | 12 | assert_equal("/", normalize_path("/foo/../")) 13 | assert_equal("/", normalize_path("/foo/..")) 14 | assert_equal("/", normalize_path("/foo/bar/../../")) 15 | assert_equal("/", normalize_path("/foo/bar/../..")) 16 | assert_equal("/", normalize_path("/foo/bar/../..")) 17 | assert_equal("/baz", normalize_path("/foo/bar/../../baz")) 18 | assert_equal("/baz", normalize_path("/foo/../bar/../baz")) 19 | assert_equal("/baz/", normalize_path("/foo/../bar/../baz/")) 20 | assert_equal("/...", normalize_path("/bar/../...")) 21 | assert_equal("/.../", normalize_path("/bar/../.../")) 22 | 23 | assert_equal("/foo/", normalize_path("/foo/./")) 24 | assert_equal("/foo/", normalize_path("/foo/.")) 25 | assert_equal("/foo/", normalize_path("/foo/././")) 26 | assert_equal("/foo/", normalize_path("/foo/./.")) 27 | assert_equal("/foo/bar", normalize_path("/foo/./bar")) 28 | assert_equal("/foo/bar/", normalize_path("/foo/./bar/.")) 29 | assert_equal("/foo/bar/", normalize_path("/./././foo/./bar/.")) 30 | 31 | assert_equal("/foo/bar/", normalize_path("//foo///.//bar/.///.//")) 32 | assert_equal("/", normalize_path("//foo///..///bar/.///..//.//")) 33 | 34 | assert_raise(RuntimeError){ normalize_path("foo/bar") } 35 | assert_raise(RuntimeError){ normalize_path("..") } 36 | assert_raise(RuntimeError){ normalize_path("/..") } 37 | assert_raise(RuntimeError){ normalize_path("/./..") } 38 | assert_raise(RuntimeError){ normalize_path("/./../") } 39 | assert_raise(RuntimeError){ normalize_path("/./../..") } 40 | assert_raise(RuntimeError){ normalize_path("/./../../") } 41 | assert_raise(RuntimeError){ normalize_path("/./../") } 42 | assert_raise(RuntimeError){ normalize_path("/../..") } 43 | assert_raise(RuntimeError){ normalize_path("/../../") } 44 | assert_raise(RuntimeError){ normalize_path("/../../..") } 45 | assert_raise(RuntimeError){ normalize_path("/../../../") } 46 | assert_raise(RuntimeError){ normalize_path("/../foo/../") } 47 | assert_raise(RuntimeError){ normalize_path("/../foo/../../") } 48 | assert_raise(RuntimeError){ normalize_path("/foo/bar/../../../../") } 49 | assert_raise(RuntimeError){ normalize_path("/foo/../bar/../../") } 50 | assert_raise(RuntimeError){ normalize_path("/./../bar/") } 51 | assert_raise(RuntimeError){ normalize_path("/./../") } 52 | end 53 | 54 | def test_split_header_value 55 | assert_equal(['foo', 'bar'], split_header_value('foo, bar')) 56 | assert_equal(['"foo"', 'bar'], split_header_value('"foo", bar')) 57 | assert_equal(['foo', '"bar"'], split_header_value('foo, "bar"')) 58 | assert_equal(['*'], split_header_value('*')) 59 | assert_equal(['W/"xyzzy"', 'W/"r2d2xxxx"', 'W/"c3piozzzz"'], 60 | split_header_value('W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"')) 61 | end 62 | 63 | def test_escape 64 | assert_equal("/foo/bar", escape("/foo/bar")) 65 | assert_equal("/~foo/bar", escape("/~foo/bar")) 66 | assert_equal("/~foo%20bar", escape("/~foo bar")) 67 | assert_equal("/~foo%20bar", escape("/~foo bar")) 68 | assert_equal("/~foo%09bar", escape("/~foo\tbar")) 69 | assert_equal("/~foo+bar", escape("/~foo+bar")) 70 | bug8425 = '[Bug #8425] [ruby-core:55052]' 71 | assert_nothing_raised(ArgumentError, Encoding::CompatibilityError, bug8425) { 72 | assert_equal("%E3%83%AB%E3%83%93%E3%83%BC%E3%81%95%E3%82%93", escape("\u{30EB 30D3 30FC 3055 3093}")) 73 | } 74 | end 75 | 76 | def test_escape_form 77 | assert_equal("%2Ffoo%2Fbar", escape_form("/foo/bar")) 78 | assert_equal("%2F~foo%2Fbar", escape_form("/~foo/bar")) 79 | assert_equal("%2F~foo+bar", escape_form("/~foo bar")) 80 | assert_equal("%2F~foo+%2B+bar", escape_form("/~foo + bar")) 81 | end 82 | 83 | def test_unescape 84 | assert_equal("/foo/bar", unescape("%2ffoo%2fbar")) 85 | assert_equal("/~foo/bar", unescape("/%7efoo/bar")) 86 | assert_equal("/~foo/bar", unescape("%2f%7efoo%2fbar")) 87 | assert_equal("/~foo+bar", unescape("/%7efoo+bar")) 88 | end 89 | 90 | def test_unescape_form 91 | assert_equal("//foo/bar", unescape_form("/%2Ffoo/bar")) 92 | assert_equal("//foo/bar baz", unescape_form("/%2Ffoo/bar+baz")) 93 | assert_equal("/~foo/bar baz", unescape_form("/%7Efoo/bar+baz")) 94 | end 95 | 96 | def test_escape_path 97 | assert_equal("/foo/bar", escape_path("/foo/bar")) 98 | assert_equal("/foo/bar/", escape_path("/foo/bar/")) 99 | assert_equal("/%25foo/bar/", escape_path("/%foo/bar/")) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/webrick/accesslog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # accesslog.rb -- Access log handling utilities 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2002 keita yamaguchi 7 | # Copyright (c) 2002 Internet Programming with Ruby writers 8 | # 9 | # $IPR: accesslog.rb,v 1.1 2002/10/01 17:16:32 gotoyuzo Exp $ 10 | 11 | module WEBrick 12 | 13 | ## 14 | # AccessLog provides logging to various files in various formats. 15 | # 16 | # Multiple logs may be written to at the same time: 17 | # 18 | # access_log = [ 19 | # [$stderr, WEBrick::AccessLog::COMMON_LOG_FORMAT], 20 | # [$stderr, WEBrick::AccessLog::REFERER_LOG_FORMAT], 21 | # ] 22 | # 23 | # server = WEBrick::HTTPServer.new :AccessLog => access_log 24 | # 25 | # Custom log formats may be defined. WEBrick::AccessLog provides a subset 26 | # of the formatting from Apache's mod_log_config 27 | # http://httpd.apache.org/docs/mod/mod_log_config.html#formats. See 28 | # AccessLog::setup_params for a list of supported options 29 | 30 | module AccessLog 31 | 32 | ## 33 | # Raised if a parameter such as %e, %i, %o or %n is used without fetching 34 | # a specific field. 35 | 36 | class AccessLogError < StandardError; end 37 | 38 | ## 39 | # The Common Log Format's time format 40 | 41 | CLF_TIME_FORMAT = "[%d/%b/%Y:%H:%M:%S %Z]" 42 | 43 | ## 44 | # Common Log Format 45 | 46 | COMMON_LOG_FORMAT = "%h %l %u %t \"%r\" %s %b" 47 | 48 | ## 49 | # Short alias for Common Log Format 50 | 51 | CLF = COMMON_LOG_FORMAT 52 | 53 | ## 54 | # Referer Log Format 55 | 56 | REFERER_LOG_FORMAT = "%{Referer}i -> %U" 57 | 58 | ## 59 | # User-Agent Log Format 60 | 61 | AGENT_LOG_FORMAT = "%{User-Agent}i" 62 | 63 | ## 64 | # Combined Log Format 65 | 66 | COMBINED_LOG_FORMAT = "#{CLF} \"%{Referer}i\" \"%{User-agent}i\"" 67 | 68 | module_function 69 | 70 | # This format specification is a subset of mod_log_config of Apache: 71 | # 72 | # %a:: Remote IP address 73 | # %b:: Total response size 74 | # %e{variable}:: Given variable in ENV 75 | # %f:: Response filename 76 | # %h:: Remote host name 77 | # %{header}i:: Given request header 78 | # %l:: Remote logname, always "-" 79 | # %m:: Request method 80 | # %{attr}n:: Given request attribute from req.attributes 81 | # %{header}o:: Given response header 82 | # %p:: Server's request port 83 | # %{format}p:: The canonical port of the server serving the request or the 84 | # actual port or the client's actual port. Valid formats are 85 | # canonical, local or remote. 86 | # %q:: Request query string 87 | # %r:: First line of the request 88 | # %s:: Request status 89 | # %t:: Time the request was received 90 | # %T:: Time taken to process the request 91 | # %u:: Remote user from auth 92 | # %U:: Unparsed URI 93 | # %%:: Literal % 94 | 95 | def setup_params(config, req, res) 96 | params = Hash.new("") 97 | params["a"] = req.peeraddr[3] 98 | params["b"] = res.sent_size 99 | params["e"] = ENV 100 | params["f"] = res.filename || "" 101 | params["h"] = req.peeraddr[2] 102 | params["i"] = req 103 | params["l"] = "-" 104 | params["m"] = req.request_method 105 | params["n"] = req.attributes 106 | params["o"] = res 107 | params["p"] = req.port 108 | params["q"] = req.query_string 109 | params["r"] = req.request_line.sub(/\x0d?\x0a\z/o, '') 110 | params["s"] = res.status # won't support "%>s" 111 | params["t"] = req.request_time 112 | params["T"] = Time.now - req.request_time 113 | params["u"] = req.user || "-" 114 | params["U"] = req.unparsed_uri 115 | params["v"] = config[:ServerName] 116 | params 117 | end 118 | 119 | ## 120 | # Formats +params+ according to +format_string+ which is described in 121 | # setup_params. 122 | 123 | def format(format_string, params) 124 | format_string.gsub(/\%(?:\{(.*?)\})?>?([a-zA-Z%])/){ 125 | param, spec = $1, $2 126 | case spec[0] 127 | when ?e, ?i, ?n, ?o 128 | raise AccessLogError, 129 | "parameter is required for \"#{spec}\"" unless param 130 | (param = params[spec][param]) ? escape(param) : "-" 131 | when ?t 132 | params[spec].strftime(param || CLF_TIME_FORMAT) 133 | when ?p 134 | case param 135 | when 'remote' 136 | escape(params["i"].peeraddr[1].to_s) 137 | else 138 | escape(params["p"].to_s) 139 | end 140 | when ?% 141 | "%" 142 | else 143 | escape(params[spec].to_s) 144 | end 145 | } 146 | end 147 | 148 | ## 149 | # Escapes control characters in +data+ 150 | 151 | def escape(data) 152 | data = data.gsub(/[[:cntrl:]\\]+/) {$&.dump[1...-1]} 153 | data.untaint if RUBY_VERSION < '2.7' 154 | data 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/webrick/httpauth/htpasswd.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpauth/htpasswd -- Apache compatible htpasswd file 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 7 | # reserved. 8 | # 9 | # $IPR: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ 10 | 11 | require_relative 'userdb' 12 | require_relative 'basicauth' 13 | require 'tempfile' 14 | 15 | module WEBrick 16 | module HTTPAuth 17 | 18 | ## 19 | # Htpasswd accesses apache-compatible password files. Passwords are 20 | # matched to a realm where they are valid. For security, the path for a 21 | # password database should be stored outside of the paths available to the 22 | # HTTP server. 23 | # 24 | # Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth. 25 | # 26 | # To create an Htpasswd database with a single user: 27 | # 28 | # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' 29 | # htpasswd.set_passwd 'my realm', 'username', 'password' 30 | # htpasswd.flush 31 | 32 | class Htpasswd 33 | include UserDB 34 | 35 | ## 36 | # Open a password database at +path+ 37 | 38 | def initialize(path, password_hash: nil) 39 | @path = path 40 | @mtime = Time.at(0) 41 | @passwd = Hash.new 42 | @auth_type = BasicAuth 43 | @password_hash = password_hash 44 | 45 | case @password_hash 46 | when nil 47 | # begin 48 | # require "string/crypt" 49 | # rescue LoadError 50 | # warn("Unable to load string/crypt, proceeding with deprecated use of String#crypt, consider using password_hash: :bcrypt") 51 | # end 52 | @password_hash = :crypt 53 | when :crypt 54 | # require "string/crypt" 55 | when :bcrypt 56 | require "bcrypt" 57 | else 58 | raise ArgumentError, "only :crypt and :bcrypt are supported for password_hash keyword argument" 59 | end 60 | 61 | File.open(@path,"a").close unless File.exist?(@path) 62 | reload 63 | end 64 | 65 | ## 66 | # Reload passwords from the database 67 | 68 | def reload 69 | mtime = File::mtime(@path) 70 | if mtime > @mtime 71 | @passwd.clear 72 | File.open(@path){|io| 73 | while line = io.gets 74 | line.chomp! 75 | case line 76 | when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z! 77 | if @password_hash == :bcrypt 78 | raise StandardError, ".htpasswd file contains crypt password, only bcrypt passwords supported" 79 | end 80 | user, pass = line.split(":") 81 | when %r!\A[^:]+:\$2[aby]\$\d{2}\$.{53}\z! 82 | if @password_hash == :crypt 83 | raise StandardError, ".htpasswd file contains bcrypt password, only crypt passwords supported" 84 | end 85 | user, pass = line.split(":") 86 | when /:\$/, /:{SHA}/ 87 | raise NotImplementedError, 88 | 'MD5, SHA1 .htpasswd file not supported' 89 | else 90 | raise StandardError, 'bad .htpasswd file' 91 | end 92 | @passwd[user] = pass 93 | end 94 | } 95 | @mtime = mtime 96 | end 97 | end 98 | 99 | ## 100 | # Flush the password database. If +output+ is given the database will 101 | # be written there instead of to the original path. 102 | 103 | def flush(output=nil) 104 | output ||= @path 105 | tmp = Tempfile.create("htpasswd", File::dirname(output)) 106 | renamed = false 107 | begin 108 | each{|item| tmp.puts(item.join(":")) } 109 | tmp.close 110 | File::rename(tmp.path, output) 111 | renamed = true 112 | ensure 113 | tmp.close 114 | File.unlink(tmp.path) if !renamed 115 | end 116 | end 117 | 118 | ## 119 | # Retrieves a password from the database for +user+ in +realm+. If 120 | # +reload_db+ is true the database will be reloaded first. 121 | 122 | def get_passwd(realm, user, reload_db) 123 | reload() if reload_db 124 | @passwd[user] 125 | end 126 | 127 | ## 128 | # Sets a password in the database for +user+ in +realm+ to +pass+. 129 | 130 | def set_passwd(realm, user, pass) 131 | if @password_hash == :bcrypt 132 | # Cost of 5 to match Apache default, and because the 133 | # bcrypt default of 10 will introduce significant delays 134 | # for every request. 135 | @passwd[user] = BCrypt::Password.create(pass, :cost=>5) 136 | else 137 | @passwd[user] = make_passwd(realm, user, pass) 138 | end 139 | end 140 | 141 | ## 142 | # Removes a password from the database for +user+ in +realm+. 143 | 144 | def delete_passwd(realm, user) 145 | @passwd.delete(user) 146 | end 147 | 148 | ## 149 | # Iterate passwords in the database. 150 | 151 | def each # :yields: [user, password] 152 | @passwd.keys.sort.each{|user| 153 | yield([user, @passwd[user]]) 154 | } 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /test/webrick/test_cookie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "webrick/cookie" 4 | 5 | class TestWEBrickCookie < Test::Unit::TestCase 6 | def test_new 7 | cookie = WEBrick::Cookie.new("foo","bar") 8 | assert_equal("foo", cookie.name) 9 | assert_equal("bar", cookie.value) 10 | assert_equal("foo=bar", cookie.to_s) 11 | end 12 | 13 | def test_time 14 | cookie = WEBrick::Cookie.new("foo","bar") 15 | t = 1000000000 16 | cookie.max_age = t 17 | assert_match(t.to_s, cookie.to_s) 18 | 19 | cookie = WEBrick::Cookie.new("foo","bar") 20 | t = Time.at(1000000000) 21 | cookie.expires = t 22 | assert_equal(Time, cookie.expires.class) 23 | assert_equal(t, cookie.expires) 24 | ts = t.httpdate 25 | cookie.expires = ts 26 | assert_equal(Time, cookie.expires.class) 27 | assert_equal(t, cookie.expires) 28 | assert_match(ts, cookie.to_s) 29 | end 30 | 31 | def test_parse 32 | data = "" 33 | data << '$Version="1"; ' 34 | data << 'Customer="WILE_E_COYOTE"; $Path="/acme"; ' 35 | data << 'Part_Number="Rocket_Launcher_0001"; $Path="/acme"; ' 36 | data << 'Shipping="FedEx"; $Path="/acme"' 37 | cookies = WEBrick::Cookie.parse(data) 38 | assert_equal(3, cookies.size) 39 | assert_equal(1, cookies[0].version) 40 | assert_equal("Customer", cookies[0].name) 41 | assert_equal("WILE_E_COYOTE", cookies[0].value) 42 | assert_equal("/acme", cookies[0].path) 43 | assert_equal(1, cookies[1].version) 44 | assert_equal("Part_Number", cookies[1].name) 45 | assert_equal("Rocket_Launcher_0001", cookies[1].value) 46 | assert_equal(1, cookies[2].version) 47 | assert_equal("Shipping", cookies[2].name) 48 | assert_equal("FedEx", cookies[2].value) 49 | 50 | data = "hoge=moge; __div__session=9865ecfd514be7f7" 51 | cookies = WEBrick::Cookie.parse(data) 52 | assert_equal(2, cookies.size) 53 | assert_equal(0, cookies[0].version) 54 | assert_equal("hoge", cookies[0].name) 55 | assert_equal("moge", cookies[0].value) 56 | assert_equal("__div__session", cookies[1].name) 57 | assert_equal("9865ecfd514be7f7", cookies[1].value) 58 | 59 | # don't allow ,-separator 60 | data = "hoge=moge, __div__session=9865ecfd514be7f7" 61 | cookies = WEBrick::Cookie.parse(data) 62 | assert_equal(1, cookies.size) 63 | assert_equal(0, cookies[0].version) 64 | assert_equal("hoge", cookies[0].name) 65 | assert_equal("moge, __div__session=9865ecfd514be7f7", cookies[0].value) 66 | end 67 | 68 | def test_parse_no_whitespace 69 | data = [ 70 | '$Version="1"; ', 71 | 'Customer="WILE_E_COYOTE";$Path="/acme";', # no SP between cookie-string 72 | 'Part_Number="Rocket_Launcher_0001";$Path="/acme";', # no SP between cookie-string 73 | 'Shipping="FedEx";$Path="/acme"' 74 | ].join 75 | cookies = WEBrick::Cookie.parse(data) 76 | assert_equal(1, cookies.size) 77 | end 78 | 79 | def test_parse_too_much_whitespaces 80 | # According to RFC6265, 81 | # cookie-string = cookie-pair *( ";" SP cookie-pair ) 82 | # So single 0x20 is needed after ';'. We allow multiple spaces here for 83 | # compatibility with older WEBrick versions. 84 | data = [ 85 | '$Version="1"; ', 86 | 'Customer="WILE_E_COYOTE";$Path="/acme"; ', # no SP between cookie-string 87 | 'Part_Number="Rocket_Launcher_0001";$Path="/acme"; ', # no SP between cookie-string 88 | 'Shipping="FedEx";$Path="/acme"' 89 | ].join 90 | cookies = WEBrick::Cookie.parse(data) 91 | assert_equal(3, cookies.size) 92 | end 93 | 94 | def test_parse_set_cookie 95 | data = %(Customer="WILE_E_COYOTE"; Version="1"; Path="/acme") 96 | cookie = WEBrick::Cookie.parse_set_cookie(data) 97 | assert_equal("Customer", cookie.name) 98 | assert_equal("WILE_E_COYOTE", cookie.value) 99 | assert_equal(1, cookie.version) 100 | assert_equal("/acme", cookie.path) 101 | 102 | data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure) 103 | cookie = WEBrick::Cookie.parse_set_cookie(data) 104 | assert_equal("Shipping", cookie.name) 105 | assert_equal("FedEx", cookie.value) 106 | assert_equal(1, cookie.version) 107 | assert_equal("/acme", cookie.path) 108 | assert_equal(true, cookie.secure) 109 | end 110 | 111 | def test_parse_set_cookies 112 | data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure) 113 | data << %(, CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT; path=/; Secure) 114 | data << %(, name="Aaron"; Version="1"; path="/acme") 115 | cookies = WEBrick::Cookie.parse_set_cookies(data) 116 | assert_equal(3, cookies.length) 117 | 118 | fed_ex = cookies.find { |c| c.name == 'Shipping' } 119 | assert_not_nil(fed_ex) 120 | assert_equal("Shipping", fed_ex.name) 121 | assert_equal("FedEx", fed_ex.value) 122 | assert_equal(1, fed_ex.version) 123 | assert_equal("/acme", fed_ex.path) 124 | assert_equal(true, fed_ex.secure) 125 | 126 | name = cookies.find { |c| c.name == 'name' } 127 | assert_not_nil(name) 128 | assert_equal("name", name.name) 129 | assert_equal("Aaron", name.value) 130 | assert_equal(1, name.version) 131 | assert_equal("/acme", name.path) 132 | 133 | customer = cookies.find { |c| c.name == 'CUSTOMER' } 134 | assert_not_nil(customer) 135 | assert_equal("CUSTOMER", customer.name) 136 | assert_equal("WILE_E_COYOTE", customer.value) 137 | assert_equal(0, customer.version) 138 | assert_equal("/", customer.path) 139 | assert_equal(Time.utc(1999, 11, 9, 23, 12, 40), customer.expires) 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/webrick/httpstatus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | #-- 3 | # httpstatus.rb -- HTTPStatus Class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: httpstatus.rb,v 1.11 2003/03/24 20:18:55 gotoyuzo Exp $ 11 | 12 | require_relative 'accesslog' 13 | 14 | module WEBrick 15 | 16 | ## 17 | # This module is used to manager HTTP status codes. 18 | # 19 | # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for more 20 | # information. 21 | module HTTPStatus 22 | 23 | ## 24 | # Root of the HTTP status class hierarchy 25 | class Status < StandardError 26 | class << self 27 | attr_reader :code, :reason_phrase # :nodoc: 28 | end 29 | 30 | # Returns the HTTP status code 31 | def code() self::class::code end 32 | 33 | # Returns the HTTP status description 34 | def reason_phrase() self::class::reason_phrase end 35 | 36 | alias to_i code # :nodoc: 37 | end 38 | 39 | # Root of the HTTP info statuses 40 | class Info < Status; end 41 | # Root of the HTTP success statuses 42 | class Success < Status; end 43 | # Root of the HTTP redirect statuses 44 | class Redirect < Status; end 45 | # Root of the HTTP error statuses 46 | class Error < Status; end 47 | # Root of the HTTP client error statuses 48 | class ClientError < Error; end 49 | # Root of the HTTP server error statuses 50 | class ServerError < Error; end 51 | 52 | class EOFError < StandardError; end 53 | 54 | # HTTP status codes and descriptions 55 | StatusMessage = { # :nodoc: 56 | 100 => 'Continue', 57 | 101 => 'Switching Protocols', 58 | 200 => 'OK', 59 | 201 => 'Created', 60 | 202 => 'Accepted', 61 | 203 => 'Non-Authoritative Information', 62 | 204 => 'No Content', 63 | 205 => 'Reset Content', 64 | 206 => 'Partial Content', 65 | 207 => 'Multi-Status', 66 | 300 => 'Multiple Choices', 67 | 301 => 'Moved Permanently', 68 | 302 => 'Found', 69 | 303 => 'See Other', 70 | 304 => 'Not Modified', 71 | 305 => 'Use Proxy', 72 | 307 => 'Temporary Redirect', 73 | 400 => 'Bad Request', 74 | 401 => 'Unauthorized', 75 | 402 => 'Payment Required', 76 | 403 => 'Forbidden', 77 | 404 => 'Not Found', 78 | 405 => 'Method Not Allowed', 79 | 406 => 'Not Acceptable', 80 | 407 => 'Proxy Authentication Required', 81 | 408 => 'Request Timeout', 82 | 409 => 'Conflict', 83 | 410 => 'Gone', 84 | 411 => 'Length Required', 85 | 412 => 'Precondition Failed', 86 | 413 => 'Request Entity Too Large', 87 | 414 => 'Request-URI Too Large', 88 | 415 => 'Unsupported Media Type', 89 | 416 => 'Request Range Not Satisfiable', 90 | 417 => 'Expectation Failed', 91 | 422 => 'Unprocessable Entity', 92 | 423 => 'Locked', 93 | 424 => 'Failed Dependency', 94 | 426 => 'Upgrade Required', 95 | 428 => 'Precondition Required', 96 | 429 => 'Too Many Requests', 97 | 431 => 'Request Header Fields Too Large', 98 | 451 => 'Unavailable For Legal Reasons', 99 | 500 => 'Internal Server Error', 100 | 501 => 'Not Implemented', 101 | 502 => 'Bad Gateway', 102 | 503 => 'Service Unavailable', 103 | 504 => 'Gateway Timeout', 104 | 505 => 'HTTP Version Not Supported', 105 | 507 => 'Insufficient Storage', 106 | 511 => 'Network Authentication Required', 107 | } 108 | 109 | # Maps a status code to the corresponding Status class 110 | CodeToError = {} # :nodoc: 111 | 112 | # Creates a status or error class for each status code and 113 | # populates the CodeToError map. 114 | StatusMessage.each{|code, message| 115 | message.freeze 116 | var_name = message.gsub(/[ \-]/,'_').upcase 117 | err_name = message.gsub(/[ \-]/,'') 118 | 119 | case code 120 | when 100...200; parent = Info 121 | when 200...300; parent = Success 122 | when 300...400; parent = Redirect 123 | when 400...500; parent = ClientError 124 | when 500...600; parent = ServerError 125 | end 126 | 127 | const_set("RC_#{var_name}", code) 128 | err_class = Class.new(parent) 129 | err_class.instance_variable_set(:@code, code) 130 | err_class.instance_variable_set(:@reason_phrase, message) 131 | const_set(err_name, err_class) 132 | CodeToError[code] = err_class 133 | } 134 | 135 | ## 136 | # Returns the description corresponding to the HTTP status +code+ 137 | # 138 | # WEBrick::HTTPStatus.reason_phrase 404 139 | # => "Not Found" 140 | def reason_phrase(code) 141 | StatusMessage[code.to_i] 142 | end 143 | 144 | ## 145 | # Is +code+ an informational status? 146 | def info?(code) 147 | code.to_i >= 100 and code.to_i < 200 148 | end 149 | 150 | ## 151 | # Is +code+ a successful status? 152 | def success?(code) 153 | code.to_i >= 200 and code.to_i < 300 154 | end 155 | 156 | ## 157 | # Is +code+ a redirection status? 158 | def redirect?(code) 159 | code.to_i >= 300 and code.to_i < 400 160 | end 161 | 162 | ## 163 | # Is +code+ an error status? 164 | def error?(code) 165 | code.to_i >= 400 and code.to_i < 600 166 | end 167 | 168 | ## 169 | # Is +code+ a client error status? 170 | def client_error?(code) 171 | code.to_i >= 400 and code.to_i < 500 172 | end 173 | 174 | ## 175 | # Is +code+ a server error status? 176 | def server_error?(code) 177 | code.to_i >= 500 and code.to_i < 600 178 | end 179 | 180 | ## 181 | # Returns the status class corresponding to +code+ 182 | # 183 | # WEBrick::HTTPStatus[302] 184 | # => WEBrick::HTTPStatus::NotFound 185 | # 186 | def self.[](code) 187 | CodeToError[code] 188 | end 189 | 190 | module_function :reason_phrase 191 | module_function :info?, :success?, :redirect?, :error? 192 | module_function :client_error?, :server_error? 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /test/webrick/test_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require "tempfile" 4 | require "webrick" 5 | require_relative "utils" 6 | 7 | class TestWEBrickServer < Test::Unit::TestCase 8 | class Echo < WEBrick::GenericServer 9 | def run(sock) 10 | while line = sock.gets 11 | sock << line 12 | end 13 | end 14 | end 15 | 16 | def test_server 17 | TestWEBrick.start_server(Echo){|server, addr, port, log| 18 | TCPSocket.open(addr, port){|sock| 19 | sock.puts("foo"); assert_equal("foo\n", sock.gets, log.call) 20 | sock.puts("bar"); assert_equal("bar\n", sock.gets, log.call) 21 | sock.puts("baz"); assert_equal("baz\n", sock.gets, log.call) 22 | sock.puts("qux"); assert_equal("qux\n", sock.gets, log.call) 23 | } 24 | } 25 | end 26 | 27 | def test_start_exception 28 | stopped = 0 29 | 30 | log = [] 31 | logger = WEBrick::Log.new(log, WEBrick::BasicLog::WARN) 32 | 33 | assert_raise(SignalException) do 34 | listener = Object.new 35 | def listener.to_io # IO.select invokes #to_io. 36 | raise SignalException, 'SIGTERM' # simulate signal in main thread 37 | end 38 | def listener.shutdown 39 | end 40 | def listener.close 41 | end 42 | 43 | server = WEBrick::HTTPServer.new({ 44 | :BindAddress => "127.0.0.1", :Port => 0, 45 | :StopCallback => Proc.new{ stopped += 1 }, 46 | :Logger => logger, 47 | }) 48 | server.listeners[0].close 49 | server.listeners[0] = listener 50 | 51 | server.start 52 | end 53 | 54 | assert_equal(1, stopped) 55 | assert_equal(1, log.length) 56 | assert_match(/FATAL SignalException: SIGTERM/, log[0]) 57 | end 58 | 59 | def test_callbacks 60 | accepted = started = stopped = 0 61 | config = { 62 | :AcceptCallback => Proc.new{ accepted += 1 }, 63 | :StartCallback => Proc.new{ started += 1 }, 64 | :StopCallback => Proc.new{ stopped += 1 }, 65 | } 66 | TestWEBrick.start_server(Echo, config){|server, addr, port, log| 67 | true while server.status != :Running 68 | sleep 1 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # server.status behaves unexpectedly with --jit-wait 69 | assert_equal(1, started, log.call) 70 | assert_equal(0, stopped, log.call) 71 | assert_equal(0, accepted, log.call) 72 | TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } 73 | TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } 74 | TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } 75 | assert_equal(3, accepted, log.call) 76 | } 77 | assert_equal(1, started) 78 | assert_equal(1, stopped) 79 | end 80 | 81 | def test_daemon 82 | begin 83 | r, w = IO.pipe 84 | pid1 = Process.fork{ 85 | r.close 86 | WEBrick::Daemon.start 87 | w.puts(Process.pid) 88 | sleep 10 89 | } 90 | pid2 = r.gets.to_i 91 | assert(Process.kill(:KILL, pid2)) 92 | assert_not_equal(pid1, pid2) 93 | rescue NotImplementedError 94 | # snip this test 95 | ensure 96 | Process.wait(pid1) if pid1 97 | r.close 98 | w.close 99 | end 100 | end 101 | 102 | def test_restart_after_shutdown 103 | address = '127.0.0.1' 104 | port = 0 105 | log = [] 106 | config = { 107 | :BindAddress => address, 108 | :Port => port, 109 | :Logger => WEBrick::Log.new(log, WEBrick::BasicLog::WARN), 110 | } 111 | server = Echo.new(config) 112 | client_proc = lambda {|str| 113 | begin 114 | ret = server.listeners.first.connect_address.connect {|s| 115 | s.write(str) 116 | s.close_write 117 | s.read 118 | } 119 | assert_equal(str, ret) 120 | ensure 121 | server.shutdown 122 | end 123 | } 124 | server_thread = Thread.new { server.start } 125 | client_thread = Thread.new { client_proc.call("a") } 126 | assert_join_threads([client_thread, server_thread]) 127 | server.listen(address, port) 128 | server_thread = Thread.new { server.start } 129 | client_thread = Thread.new { client_proc.call("b") } 130 | assert_join_threads([client_thread, server_thread]) 131 | assert_equal([], log) 132 | end 133 | 134 | def test_restart_after_stop 135 | log = Object.new 136 | class << log 137 | include Test::Unit::Assertions 138 | def <<(msg) 139 | flunk "unexpected log: #{msg.inspect}" 140 | end 141 | end 142 | client_thread = nil 143 | wakeup = -> {client_thread.wakeup} 144 | warn_flunk = WEBrick::Log.new(log, WEBrick::BasicLog::WARN) 145 | server = WEBrick::HTTPServer.new( 146 | :StartCallback => wakeup, 147 | :StopCallback => wakeup, 148 | :BindAddress => '0.0.0.0', 149 | :Port => 0, 150 | :Logger => warn_flunk) 151 | 2.times { 152 | server_thread = Thread.start { 153 | server.start 154 | } 155 | client_thread = Thread.start { 156 | sleep 0.1 until server.status == :Running || !server_thread.status 157 | server.stop 158 | sleep 0.1 until server.status == :Stop || !server_thread.status 159 | } 160 | assert_join_threads([client_thread, server_thread]) 161 | } 162 | end 163 | 164 | def test_port_numbers 165 | config = { 166 | :BindAddress => '0.0.0.0', 167 | :Logger => WEBrick::Log.new([], WEBrick::BasicLog::WARN), 168 | } 169 | 170 | ports = [0, "0"] 171 | 172 | ports.each do |port| 173 | config[:Port]= port 174 | server = WEBrick::GenericServer.new(config) 175 | server_thread = Thread.start { server.start } 176 | client_thread = Thread.start { 177 | sleep 0.1 until server.status == :Running || !server_thread.status 178 | server_port = server.listeners[0].addr[1] 179 | server.stop 180 | assert_equal server.config[:Port], server_port 181 | sleep 0.1 until server.status == :Stop || !server_thread.status 182 | } 183 | assert_join_threads([client_thread, server_thread]) 184 | end 185 | 186 | assert_raise(ArgumentError) do 187 | config[:Port]= "FOO" 188 | WEBrick::GenericServer.new(config) 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/webrick/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # config.rb -- Default configurations. 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2003 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: config.rb,v 1.52 2003/07/22 19:20:42 gotoyuzo Exp $ 11 | 12 | require_relative 'version' 13 | require_relative 'httpversion' 14 | require_relative 'httputils' 15 | require_relative 'utils' 16 | require_relative 'log' 17 | 18 | module WEBrick 19 | module Config 20 | LIBDIR = File::dirname(__FILE__) # :nodoc: 21 | 22 | # for GenericServer 23 | General = Hash.new { |hash, key| 24 | case key 25 | when :ServerName 26 | hash[key] = Utils.getservername 27 | else 28 | nil 29 | end 30 | }.update( 31 | :BindAddress => nil, # "0.0.0.0" or "::" or nil 32 | :Port => nil, # users MUST specify this!! 33 | :MaxClients => 100, # maximum number of the concurrent connections 34 | :ServerType => nil, # default: WEBrick::SimpleServer 35 | :Logger => nil, # default: WEBrick::Log.new 36 | :ServerSoftware => "WEBrick/#{WEBrick::VERSION} " + 37 | "(Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})", 38 | :TempDir => ENV['TMPDIR']||ENV['TMP']||ENV['TEMP']||'/tmp', 39 | :DoNotListen => false, 40 | :StartCallback => nil, 41 | :StopCallback => nil, 42 | :AcceptCallback => nil, 43 | :DoNotReverseLookup => true, 44 | :ShutdownSocketWithoutClose => false, 45 | ) 46 | 47 | # for HTTPServer, HTTPRequest, HTTPResponse ... 48 | HTTP = General.dup.update( 49 | :Port => 80, 50 | :RequestTimeout => 30, 51 | :HTTPVersion => HTTPVersion.new("1.1"), 52 | :AccessLog => nil, 53 | :MimeTypes => HTTPUtils::DefaultMimeTypes, 54 | :DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"], 55 | :DocumentRoot => nil, 56 | :DocumentRootOptions => { :FancyIndexing => true }, 57 | :RequestCallback => nil, 58 | :ServerAlias => nil, 59 | :InputBufferSize => 65536, # input buffer size in reading request body 60 | :OutputBufferSize => 65536, # output buffer size in sending File or IO 61 | 62 | # for HTTPProxyServer 63 | :ProxyAuthProc => nil, 64 | :ProxyContentHandler => nil, 65 | :ProxyVia => true, 66 | :ProxyTimeout => true, 67 | :ProxyURI => nil, 68 | 69 | :CGIInterpreter => nil, 70 | :CGIPathEnv => nil, 71 | 72 | # workaround: if Request-URIs contain 8bit chars, 73 | # they should be escaped before calling of URI::parse(). 74 | :Escape8bitURI => false 75 | ) 76 | 77 | ## 78 | # Default configuration for WEBrick::HTTPServlet::FileHandler 79 | # 80 | # :AcceptableLanguages:: 81 | # Array of languages allowed for accept-language. There is no default 82 | # :DirectoryCallback:: 83 | # Allows preprocessing of directory requests. There is no default 84 | # callback. 85 | # :FancyIndexing:: 86 | # If true, show an index for directories. The default is true. 87 | # :FileCallback:: 88 | # Allows preprocessing of file requests. There is no default callback. 89 | # :HandlerCallback:: 90 | # Allows preprocessing of requests. There is no default callback. 91 | # :HandlerTable:: 92 | # Maps file suffixes to file handlers. DefaultFileHandler is used by 93 | # default but any servlet can be used. 94 | # :NondisclosureName:: 95 | # Do not show files matching this array of globs. .ht* and *~ are 96 | # excluded by default. 97 | # :UserDir:: 98 | # Directory inside ~user to serve content from for /~user requests. 99 | # Only works if mounted on /. Disabled by default. 100 | 101 | FileHandler = { 102 | :NondisclosureName => [".ht*", "*~"], 103 | :FancyIndexing => false, 104 | :HandlerTable => {}, 105 | :HandlerCallback => nil, 106 | :DirectoryCallback => nil, 107 | :FileCallback => nil, 108 | :UserDir => nil, # e.g. "public_html" 109 | :AcceptableLanguages => [] # ["en", "ja", ... ] 110 | } 111 | 112 | ## 113 | # Default configuration for WEBrick::HTTPAuth::BasicAuth 114 | # 115 | # :AutoReloadUserDB:: Reload the user database provided by :UserDB 116 | # automatically? 117 | 118 | BasicAuth = { 119 | :AutoReloadUserDB => true, 120 | } 121 | 122 | ## 123 | # Default configuration for WEBrick::HTTPAuth::DigestAuth. 124 | # 125 | # :Algorithm:: MD5, MD5-sess (default), SHA1, SHA1-sess 126 | # :Domain:: An Array of URIs that define the protected space 127 | # :Qop:: 'auth' for authentication, 'auth-int' for integrity protection or 128 | # both 129 | # :UseOpaque:: Should the server send opaque values to the client? This 130 | # helps prevent replay attacks. 131 | # :CheckNc:: Should the server check the nonce count? This helps the 132 | # server detect replay attacks. 133 | # :UseAuthenticationInfoHeader:: Should the server send an 134 | # AuthenticationInfo header? 135 | # :AutoReloadUserDB:: Reload the user database provided by :UserDB 136 | # automatically? 137 | # :NonceExpirePeriod:: How long should we store used nonces? Default is 138 | # 30 minutes. 139 | # :NonceExpireDelta:: How long is a nonce valid? Default is 1 minute 140 | # :InternetExplorerHack:: Hack which allows Internet Explorer to work. 141 | # :OperaHack:: Hack which allows Opera to work. 142 | 143 | DigestAuth = { 144 | :Algorithm => 'MD5-sess', # or 'MD5' 145 | :Domain => nil, # an array includes domain names. 146 | :Qop => [ 'auth' ], # 'auth' or 'auth-int' or both. 147 | :UseOpaque => true, 148 | :UseNextNonce => false, 149 | :CheckNc => false, 150 | :UseAuthenticationInfoHeader => true, 151 | :AutoReloadUserDB => true, 152 | :NonceExpirePeriod => 30*60, 153 | :NonceExpireDelta => 60, 154 | :InternetExplorerHack => true, 155 | :OperaHack => true, 156 | } 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /test/webrick/test_cgi.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require_relative "utils" 4 | require "webrick" 5 | require "test/unit" 6 | 7 | class TestWEBrickCGI < Test::Unit::TestCase 8 | CRLF = "\r\n" 9 | 10 | def teardown 11 | WEBrick::Utils::TimeoutHandler.terminate 12 | super 13 | end 14 | 15 | def start_cgi_server(log_tester=TestWEBrick::DefaultLogTester, &block) 16 | config = { 17 | :CGIInterpreter => TestWEBrick::RubyBin, 18 | :DocumentRoot => File.dirname(__FILE__), 19 | :DirectoryIndex => ["webrick.cgi"], 20 | :RequestCallback => Proc.new{|req, res| 21 | def req.meta_vars 22 | meta = super 23 | meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR) 24 | meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV'] 25 | return meta 26 | end 27 | }, 28 | } 29 | if RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32/ 30 | config[:CGIPathEnv] = ENV['PATH'] # runtime dll may not be in system dir. 31 | end 32 | TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| 33 | block.call(server, addr, port, log) 34 | } 35 | end 36 | 37 | def test_cgi 38 | start_cgi_server{|server, addr, port, log| 39 | http = Net::HTTP.new(addr, port) 40 | req = Net::HTTP::Get.new("/webrick.cgi") 41 | http.request(req){|res| assert_equal("/webrick.cgi", res.body, log.call)} 42 | req = Net::HTTP::Get.new("/webrick.cgi/path/info") 43 | http.request(req){|res| assert_equal("/path/info", res.body, log.call)} 44 | req = Net::HTTP::Get.new("/webrick.cgi/%3F%3F%3F?foo=bar") 45 | http.request(req){|res| assert_equal("/???", res.body, log.call)} 46 | unless RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32|java/ 47 | # Path info of res.body is passed via ENV. 48 | # ENV[] returns different value on Windows depending on locale. 49 | req = Net::HTTP::Get.new("/webrick.cgi/%A4%DB%A4%B2/%A4%DB%A4%B2") 50 | http.request(req){|res| 51 | assert_equal("/\xA4\xDB\xA4\xB2/\xA4\xDB\xA4\xB2", res.body, log.call)} 52 | end 53 | req = Net::HTTP::Get.new("/webrick.cgi?a=1;a=2;b=x") 54 | http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} 55 | req = Net::HTTP::Get.new("/webrick.cgi?a=1&a=2&b=x") 56 | http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} 57 | 58 | req = Net::HTTP::Post.new("/webrick.cgi?a=x;a=y;b=1") 59 | req["Content-Type"] = "application/x-www-form-urlencoded" 60 | http.request(req, "a=1;a=2;b=x"){|res| 61 | assert_equal("a=1, a=2, b=x", res.body, log.call)} 62 | req = Net::HTTP::Post.new("/webrick.cgi?a=x&a=y&b=1") 63 | req["Content-Type"] = "application/x-www-form-urlencoded" 64 | http.request(req, "a=1&a=2&b=x"){|res| 65 | assert_equal("a=1, a=2, b=x", res.body, log.call)} 66 | req = Net::HTTP::Get.new("/") 67 | http.request(req){|res| 68 | ary = res.body.lines.to_a 69 | assert_match(%r{/$}, ary[0], log.call) 70 | assert_match(%r{/webrick.cgi$}, ary[1], log.call) 71 | } 72 | 73 | req = Net::HTTP::Get.new("/webrick.cgi") 74 | req["Cookie"] = "CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001" 75 | http.request(req){|res| 76 | assert_equal( 77 | "CUSTOMER=WILE_E_COYOTE\nPART_NUMBER=ROCKET_LAUNCHER_0001\n", 78 | res.body, log.call) 79 | } 80 | 81 | req = Net::HTTP::Get.new("/webrick.cgi") 82 | cookie = %{$Version="1"; } 83 | cookie << %{Customer="WILE_E_COYOTE"; $Path="/acme"; } 84 | cookie << %{Part_Number="Rocket_Launcher_0001"; $Path="/acme"; } 85 | cookie << %{Shipping="FedEx"; $Path="/acme"} 86 | req["Cookie"] = cookie 87 | http.request(req){|res| 88 | assert_equal("Customer=WILE_E_COYOTE, Shipping=FedEx", 89 | res["Set-Cookie"], log.call) 90 | assert_equal("Customer=WILE_E_COYOTE\n" + 91 | "Part_Number=Rocket_Launcher_0001\n" + 92 | "Shipping=FedEx\n", res.body, log.call) 93 | } 94 | } 95 | end 96 | 97 | def test_bad_request 98 | log_tester = lambda {|log, access_log| 99 | assert_match(/BadRequest/, log.join) 100 | } 101 | start_cgi_server(log_tester) {|server, addr, port, log| 102 | sock = TCPSocket.new(addr, port) 103 | begin 104 | sock << "POST /webrick.cgi HTTP/1.0" << CRLF 105 | sock << "Content-Type: application/x-www-form-urlencoded" << CRLF 106 | sock << "Content-Length: 1024" << CRLF 107 | sock << CRLF 108 | sock << "a=1&a=2&b=x" 109 | sock.close_write 110 | assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, sock.read, log.call) 111 | ensure 112 | sock.close 113 | end 114 | } 115 | end 116 | 117 | def test_cgi_env 118 | start_cgi_server do |server, addr, port, log| 119 | http = Net::HTTP.new(addr, port) 120 | req = Net::HTTP::Get.new("/webrick.cgi/dumpenv") 121 | req['proxy'] = 'http://example.com/' 122 | req['hello'] = 'world' 123 | http.request(req) do |res| 124 | env = Marshal.load(res.body) 125 | assert_equal 'world', env['HTTP_HELLO'] 126 | assert_not_operator env, :include?, 'HTTP_PROXY' 127 | end 128 | end 129 | end 130 | 131 | CtrlSeq = [0x7f, *(1..31)].pack("C*").gsub(/\s+/, '') 132 | CtrlPat = /#{Regexp.quote(CtrlSeq)}/o 133 | DumpPat = /#{Regexp.quote(CtrlSeq.dump[1...-1])}/o 134 | 135 | def test_bad_uri 136 | log_tester = lambda {|log, access_log| 137 | assert_equal(1, log.length) 138 | assert_match(/ERROR bad URI/, log[0]) 139 | } 140 | start_cgi_server(log_tester) {|server, addr, port, log| 141 | res = TCPSocket.open(addr, port) {|sock| 142 | sock << "GET /#{CtrlSeq}#{CRLF}#{CRLF}" 143 | sock.close_write 144 | sock.read 145 | } 146 | assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res) 147 | s = log.call.each_line.grep(/ERROR bad URI/)[0] 148 | assert_match(DumpPat, s) 149 | assert_not_match(CtrlPat, s) 150 | } 151 | end 152 | 153 | def test_bad_header 154 | log_tester = lambda {|log, access_log| 155 | assert_equal(1, log.length) 156 | assert_match(/ERROR bad header/, log[0]) 157 | } 158 | start_cgi_server(log_tester) {|server, addr, port, log| 159 | res = TCPSocket.open(addr, port) {|sock| 160 | sock << "GET / HTTP/1.0#{CRLF}#{CtrlSeq}#{CRLF}#{CRLF}" 161 | sock.close_write 162 | sock.read 163 | } 164 | assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res) 165 | s = log.call.each_line.grep(/ERROR bad header/)[0] 166 | assert_match(DumpPat, s) 167 | assert_not_match(CtrlPat, s) 168 | } 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /lib/webrick.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | ## 3 | # = WEB server toolkit. 4 | # 5 | # WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, 6 | # a proxy server, and a virtual-host server. WEBrick features complete 7 | # logging of both server operations and HTTP access. WEBrick supports both 8 | # basic and digest authentication in addition to algorithms not in RFC 2617. 9 | # 10 | # A WEBrick server can be composed of multiple WEBrick servers or servlets to 11 | # provide differing behavior on a per-host or per-path basis. WEBrick 12 | # includes servlets for handling CGI scripts, ERB pages, Ruby blocks and 13 | # directory listings. 14 | # 15 | # WEBrick also includes tools for daemonizing a process and starting a process 16 | # at a higher privilege level and dropping permissions. 17 | # 18 | # == Security 19 | # 20 | # *Warning:* WEBrick is not recommended for production. It only implements 21 | # basic security checks. 22 | # 23 | # == Starting an HTTP server 24 | # 25 | # To create a new WEBrick::HTTPServer that will listen to connections on port 26 | # 8000 and serve documents from the current user's public_html folder: 27 | # 28 | # require 'webrick' 29 | # 30 | # root = File.expand_path '~/public_html' 31 | # server = WEBrick::HTTPServer.new :Port => 8000, :DocumentRoot => root 32 | # 33 | # To run the server you will need to provide a suitable shutdown hook as 34 | # starting the server blocks the current thread: 35 | # 36 | # trap 'INT' do server.shutdown end 37 | # 38 | # server.start 39 | # 40 | # == Custom Behavior 41 | # 42 | # The easiest way to have a server perform custom operations is through 43 | # WEBrick::HTTPServer#mount_proc. The block given will be called with a 44 | # WEBrick::HTTPRequest with request info and a WEBrick::HTTPResponse which 45 | # must be filled in appropriately: 46 | # 47 | # server.mount_proc '/' do |req, res| 48 | # res.body = 'Hello, world!' 49 | # end 50 | # 51 | # Remember that +server.mount_proc+ must precede +server.start+. 52 | # 53 | # == Servlets 54 | # 55 | # Advanced custom behavior can be obtained through mounting a subclass of 56 | # WEBrick::HTTPServlet::AbstractServlet. Servlets provide more modularity 57 | # when writing an HTTP server than mount_proc allows. Here is a simple 58 | # servlet: 59 | # 60 | # class Simple < WEBrick::HTTPServlet::AbstractServlet 61 | # def do_GET request, response 62 | # status, content_type, body = do_stuff_with request 63 | # 64 | # response.status = 200 65 | # response['Content-Type'] = 'text/plain' 66 | # response.body = 'Hello, World!' 67 | # end 68 | # end 69 | # 70 | # To initialize the servlet you mount it on the server: 71 | # 72 | # server.mount '/simple', Simple 73 | # 74 | # See WEBrick::HTTPServlet::AbstractServlet for more details. 75 | # 76 | # == Virtual Hosts 77 | # 78 | # A server can act as a virtual host for multiple host names. After creating 79 | # the listening host, additional hosts that do not listen can be created and 80 | # attached as virtual hosts: 81 | # 82 | # server = WEBrick::HTTPServer.new # ... 83 | # 84 | # vhost = WEBrick::HTTPServer.new :ServerName => 'vhost.example', 85 | # :DoNotListen => true, # ... 86 | # vhost.mount '/', ... 87 | # 88 | # server.virtual_host vhost 89 | # 90 | # If no +:DocumentRoot+ is provided and no servlets or procs are mounted on the 91 | # main server it will return 404 for all URLs. 92 | # 93 | # == HTTPS 94 | # 95 | # To create an HTTPS server you only need to enable SSL and provide an SSL 96 | # certificate name: 97 | # 98 | # require 'webrick' 99 | # require 'webrick/https' 100 | # 101 | # cert_name = [ 102 | # %w[CN localhost], 103 | # ] 104 | # 105 | # server = WEBrick::HTTPServer.new(:Port => 8000, 106 | # :SSLEnable => true, 107 | # :SSLCertName => cert_name) 108 | # 109 | # This will start the server with a self-generated self-signed certificate. 110 | # The certificate will be changed every time the server is restarted. 111 | # 112 | # To create a server with a pre-determined key and certificate you can provide 113 | # them: 114 | # 115 | # require 'webrick' 116 | # require 'webrick/https' 117 | # require 'openssl' 118 | # 119 | # cert = OpenSSL::X509::Certificate.new File.read '/path/to/cert.pem' 120 | # pkey = OpenSSL::PKey::RSA.new File.read '/path/to/pkey.pem' 121 | # 122 | # server = WEBrick::HTTPServer.new(:Port => 8000, 123 | # :SSLEnable => true, 124 | # :SSLCertificate => cert, 125 | # :SSLPrivateKey => pkey) 126 | # 127 | # == Proxy Server 128 | # 129 | # WEBrick can act as a proxy server: 130 | # 131 | # require 'webrick' 132 | # require 'webrick/httpproxy' 133 | # 134 | # proxy = WEBrick::HTTPProxyServer.new :Port => 8000 135 | # 136 | # trap 'INT' do proxy.shutdown end 137 | # 138 | # See WEBrick::HTTPProxy for further details including modifying proxied 139 | # responses. 140 | # 141 | # == Basic and Digest authentication 142 | # 143 | # WEBrick provides both Basic and Digest authentication for regular and proxy 144 | # servers. See WEBrick::HTTPAuth, WEBrick::HTTPAuth::BasicAuth and 145 | # WEBrick::HTTPAuth::DigestAuth. 146 | # 147 | # == WEBrick as a daemonized Web Server 148 | # 149 | # WEBrick can be run as a daemonized server for small loads. 150 | # 151 | # === Daemonizing 152 | # 153 | # To start a WEBrick server as a daemon simple run WEBrick::Daemon.start 154 | # before starting the server. 155 | # 156 | # === Dropping Permissions 157 | # 158 | # WEBrick can be started as one user to gain permission to bind to port 80 or 159 | # 443 for serving HTTP or HTTPS traffic then can drop these permissions for 160 | # regular operation. To listen on all interfaces for HTTP traffic: 161 | # 162 | # sockets = WEBrick::Utils.create_listeners nil, 80 163 | # 164 | # Then drop privileges: 165 | # 166 | # WEBrick::Utils.su 'www' 167 | # 168 | # Then create a server that does not listen by default: 169 | # 170 | # server = WEBrick::HTTPServer.new :DoNotListen => true, # ... 171 | # 172 | # Then overwrite the listening sockets with the port 80 sockets: 173 | # 174 | # server.listeners.replace sockets 175 | # 176 | # === Logging 177 | # 178 | # WEBrick can separately log server operations and end-user access. For 179 | # server operations: 180 | # 181 | # log_file = File.open '/var/log/webrick.log', 'a+' 182 | # log = WEBrick::Log.new log_file 183 | # 184 | # For user access logging: 185 | # 186 | # access_log = [ 187 | # [log_file, WEBrick::AccessLog::COMBINED_LOG_FORMAT], 188 | # ] 189 | # 190 | # server = WEBrick::HTTPServer.new :Logger => log, :AccessLog => access_log 191 | # 192 | # See WEBrick::AccessLog for further log formats. 193 | # 194 | # === Log Rotation 195 | # 196 | # To rotate logs in WEBrick on a HUP signal (like syslogd can send), open the 197 | # log file in 'a+' mode (as above) and trap 'HUP' to reopen the log file: 198 | # 199 | # trap 'HUP' do log_file.reopen '/path/to/webrick.log', 'a+' 200 | # 201 | # == Copyright 202 | # 203 | # Author: IPR -- Internet Programming with Ruby -- writers 204 | # 205 | # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU 206 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 207 | # reserved. 208 | #-- 209 | # $IPR: webrick.rb,v 1.12 2002/10/01 17:16:31 gotoyuzo Exp $ 210 | 211 | module WEBrick 212 | end 213 | 214 | require 'webrick/compat.rb' 215 | 216 | require 'webrick/version.rb' 217 | require 'webrick/config.rb' 218 | require 'webrick/log.rb' 219 | require 'webrick/server.rb' 220 | require_relative 'webrick/utils.rb' 221 | require 'webrick/accesslog' 222 | 223 | require 'webrick/htmlutils.rb' 224 | require 'webrick/httputils.rb' 225 | require 'webrick/cookie.rb' 226 | require 'webrick/httpversion.rb' 227 | require 'webrick/httpstatus.rb' 228 | require 'webrick/httprequest.rb' 229 | require 'webrick/httpresponse.rb' 230 | require 'webrick/httpserver.rb' 231 | require 'webrick/httpservlet.rb' 232 | require 'webrick/httpauth.rb' 233 | -------------------------------------------------------------------------------- /lib/webrick/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # utils.rb -- Miscellaneous utilities 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: utils.rb,v 1.10 2003/02/16 22:22:54 gotoyuzo Exp $ 11 | 12 | require 'socket' 13 | require 'io/nonblock' 14 | require 'etc' 15 | 16 | module WEBrick 17 | module Utils 18 | ## 19 | # Sets IO operations on +io+ to be non-blocking 20 | def set_non_blocking(io) 21 | io.nonblock = true if io.respond_to?(:nonblock=) 22 | end 23 | module_function :set_non_blocking 24 | 25 | ## 26 | # Sets the close on exec flag for +io+ 27 | def set_close_on_exec(io) 28 | io.close_on_exec = true if io.respond_to?(:close_on_exec=) 29 | end 30 | module_function :set_close_on_exec 31 | 32 | ## 33 | # Changes the process's uid and gid to the ones of +user+ 34 | def su(user) 35 | if pw = Etc.getpwnam(user) 36 | Process::initgroups(user, pw.gid) 37 | Process::Sys::setgid(pw.gid) 38 | Process::Sys::setuid(pw.uid) 39 | else 40 | warn("WEBrick::Utils::su doesn't work on this platform", uplevel: 1) 41 | end 42 | end 43 | module_function :su 44 | 45 | ## 46 | # The server hostname 47 | def getservername 48 | Socket::gethostname 49 | end 50 | module_function :getservername 51 | 52 | ## 53 | # Creates TCP server sockets bound to +address+:+port+ and returns them. 54 | # 55 | # It will create IPV4 and IPV6 sockets on all interfaces. 56 | def create_listeners(address, port) 57 | unless port 58 | raise ArgumentError, "must specify port" 59 | end 60 | sockets = Socket.tcp_server_sockets(address, port) 61 | sockets = sockets.map {|s| 62 | s.autoclose = false 63 | ts = TCPServer.for_fd(s.fileno) 64 | s.close 65 | ts 66 | } 67 | return sockets 68 | end 69 | module_function :create_listeners 70 | 71 | ## 72 | # Characters used to generate random strings 73 | RAND_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 74 | "0123456789" + 75 | "abcdefghijklmnopqrstuvwxyz" 76 | 77 | ## 78 | # Generates a random string of length +len+ 79 | def random_string(len) 80 | rand_max = RAND_CHARS.bytesize 81 | ret = +"" 82 | len.times{ ret << RAND_CHARS[rand(rand_max)] } 83 | ret 84 | end 85 | module_function :random_string 86 | 87 | ########### 88 | 89 | require "timeout" 90 | require "singleton" 91 | 92 | ## 93 | # Class used to manage timeout handlers across multiple threads. 94 | # 95 | # Timeout handlers should be managed by using the class methods which are 96 | # synchronized. 97 | # 98 | # id = TimeoutHandler.register(10, Timeout::Error) 99 | # begin 100 | # sleep 20 101 | # puts 'foo' 102 | # ensure 103 | # TimeoutHandler.cancel(id) 104 | # end 105 | # 106 | # will raise Timeout::Error 107 | # 108 | # id = TimeoutHandler.register(10, Timeout::Error) 109 | # begin 110 | # sleep 5 111 | # puts 'foo' 112 | # ensure 113 | # TimeoutHandler.cancel(id) 114 | # end 115 | # 116 | # will print 'foo' 117 | # 118 | class TimeoutHandler 119 | include Singleton 120 | 121 | ## 122 | # Mutex used to synchronize access across threads 123 | TimeoutMutex = Thread::Mutex.new # :nodoc: 124 | 125 | ## 126 | # Registers a new timeout handler 127 | # 128 | # +time+:: Timeout in seconds 129 | # +exception+:: Exception to raise when timeout elapsed 130 | def TimeoutHandler.register(seconds, exception) 131 | at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds 132 | instance.register(Thread.current, at, exception) 133 | end 134 | 135 | ## 136 | # Cancels the timeout handler +id+ 137 | def TimeoutHandler.cancel(id) 138 | instance.cancel(Thread.current, id) 139 | end 140 | 141 | def self.terminate 142 | instance.terminate 143 | end 144 | 145 | ## 146 | # Creates a new TimeoutHandler. You should use ::register and ::cancel 147 | # instead of creating the timeout handler directly. 148 | def initialize 149 | TimeoutMutex.synchronize{ 150 | @timeout_info = Hash.new 151 | } 152 | @queue = Thread::Queue.new 153 | @watcher = nil 154 | end 155 | 156 | # :nodoc: 157 | private \ 158 | def watch 159 | to_interrupt = [] 160 | while true 161 | now = Process.clock_gettime(Process::CLOCK_MONOTONIC) 162 | wakeup = nil 163 | to_interrupt.clear 164 | TimeoutMutex.synchronize{ 165 | @timeout_info.each {|thread, ary| 166 | next unless ary 167 | ary.each{|info| 168 | time, exception = *info 169 | if time < now 170 | to_interrupt.push [thread, info.object_id, exception] 171 | elsif !wakeup || time < wakeup 172 | wakeup = time 173 | end 174 | } 175 | } 176 | } 177 | to_interrupt.each {|arg| interrupt(*arg)} 178 | if !wakeup 179 | @queue.pop 180 | elsif (wakeup -= now) > 0 181 | begin 182 | (th = Thread.start {@queue.pop}).join(wakeup) 183 | ensure 184 | th&.kill&.join 185 | end 186 | end 187 | @queue.clear 188 | end 189 | end 190 | 191 | # :nodoc: 192 | private \ 193 | def watcher 194 | (w = @watcher)&.alive? and return w # usual case 195 | TimeoutMutex.synchronize{ 196 | (w = @watcher)&.alive? and next w # pathological check 197 | @watcher = Thread.start(&method(:watch)) 198 | } 199 | end 200 | 201 | ## 202 | # Interrupts the timeout handler +id+ and raises +exception+ 203 | def interrupt(thread, id, exception) 204 | if cancel(thread, id) && thread.alive? 205 | thread.raise(exception, "execution timeout") 206 | end 207 | end 208 | 209 | ## 210 | # Registers a new timeout handler 211 | # 212 | # +time+:: Timeout in seconds 213 | # +exception+:: Exception to raise when timeout elapsed 214 | def register(thread, time, exception) 215 | info = nil 216 | TimeoutMutex.synchronize{ 217 | (@timeout_info[thread] ||= []) << (info = [time, exception]) 218 | } 219 | @queue.push nil 220 | watcher 221 | return info.object_id 222 | end 223 | 224 | ## 225 | # Cancels the timeout handler +id+ 226 | def cancel(thread, id) 227 | TimeoutMutex.synchronize{ 228 | if ary = @timeout_info[thread] 229 | ary.delete_if{|info| info.object_id == id } 230 | if ary.empty? 231 | @timeout_info.delete(thread) 232 | end 233 | return true 234 | end 235 | return false 236 | } 237 | end 238 | 239 | ## 240 | def terminate 241 | TimeoutMutex.synchronize{ 242 | @timeout_info.clear 243 | @watcher&.kill&.join 244 | } 245 | end 246 | end 247 | 248 | ## 249 | # Executes the passed block and raises +exception+ if execution takes more 250 | # than +seconds+. 251 | # 252 | # If +seconds+ is zero or nil, simply executes the block 253 | def timeout(seconds, exception=Timeout::Error) 254 | return yield if seconds.nil? or seconds.zero? 255 | # raise ThreadError, "timeout within critical session" if Thread.critical 256 | id = TimeoutHandler.register(seconds, exception) 257 | begin 258 | yield(seconds) 259 | ensure 260 | TimeoutHandler.cancel(id) 261 | end 262 | end 263 | module_function :timeout 264 | end 265 | end 266 | -------------------------------------------------------------------------------- /lib/webrick/ssl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # ssl.rb -- SSL/TLS enhancement for GenericServer 4 | # 5 | # Copyright (c) 2003 GOTOU Yuuzou All rights reserved. 6 | # 7 | # $Id$ 8 | 9 | require 'webrick' 10 | require 'openssl' 11 | 12 | module WEBrick 13 | module Config 14 | svrsoft = General[:ServerSoftware] 15 | osslv = ::OpenSSL::OPENSSL_VERSION.split[1] 16 | 17 | ## 18 | # Default SSL server configuration. 19 | # 20 | # WEBrick can automatically create a self-signed certificate if 21 | # :SSLCertName is set. For more information on the various 22 | # SSL options see OpenSSL::SSL::SSLContext. 23 | # 24 | # :ServerSoftware :: 25 | # The server software name used in the Server: header. 26 | # :SSLEnable :: false, 27 | # Enable SSL for this server. Defaults to false. 28 | # :SSLCertificate :: 29 | # The SSL certificate for the server. 30 | # :SSLPrivateKey :: 31 | # The SSL private key for the server certificate. 32 | # :SSLClientCA :: nil, 33 | # Array of certificates that will be sent to the client. 34 | # :SSLExtraChainCert :: nil, 35 | # Array of certificates that will be added to the certificate chain 36 | # :SSLCACertificateFile :: nil, 37 | # Path to a CA certificate file 38 | # :SSLCACertificatePath :: nil, 39 | # Path to a directory containing CA certificates 40 | # :SSLCertificateStore :: nil, 41 | # OpenSSL::X509::Store used for certificate validation of the client 42 | # :SSLTmpDhCallback :: nil, 43 | # Callback invoked when DH parameters are required. 44 | # :SSLVerifyClient :: 45 | # Sets whether the client is verified. This defaults to VERIFY_NONE 46 | # which is typical for an HTTPS server. 47 | # :SSLVerifyDepth :: 48 | # Number of CA certificates to walk when verifying a certificate chain 49 | # :SSLVerifyCallback :: 50 | # Custom certificate verification callback 51 | # :SSLServerNameCallback:: 52 | # Custom servername indication callback 53 | # :SSLTimeout :: 54 | # Maximum session lifetime 55 | # :SSLOptions :: 56 | # Various SSL options 57 | # :SSLCiphers :: 58 | # Ciphers to be used 59 | # :SSLStartImmediately :: 60 | # Immediately start SSL upon connection? Defaults to true 61 | # :SSLCertName :: 62 | # SSL certificate name. Must be set to enable automatic certificate 63 | # creation. 64 | # :SSLCertComment :: 65 | # Comment used during automatic certificate creation. 66 | 67 | SSL = { 68 | :ServerSoftware => "#{svrsoft} OpenSSL/#{osslv}", 69 | :SSLEnable => false, 70 | :SSLCertificate => nil, 71 | :SSLPrivateKey => nil, 72 | :SSLClientCA => nil, 73 | :SSLExtraChainCert => nil, 74 | :SSLCACertificateFile => nil, 75 | :SSLCACertificatePath => nil, 76 | :SSLCertificateStore => nil, 77 | :SSLTmpDhCallback => nil, 78 | :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE, 79 | :SSLVerifyDepth => nil, 80 | :SSLVerifyCallback => nil, # custom verification 81 | :SSLTimeout => nil, 82 | :SSLOptions => nil, 83 | :SSLCiphers => nil, 84 | :SSLStartImmediately => true, 85 | # Must specify if you use auto generated certificate. 86 | :SSLCertName => nil, 87 | :SSLCertComment => "Generated by Ruby/OpenSSL" 88 | } 89 | General.update(SSL) 90 | end 91 | 92 | module Utils 93 | ## 94 | # Creates a self-signed certificate with the given number of +bits+, 95 | # the issuer +cn+ and a +comment+ to be stored in the certificate. 96 | 97 | def create_self_signed_cert(bits, cn, comment) 98 | rsa = OpenSSL::PKey::RSA.new(bits){|p, n| 99 | case p 100 | when 0; $stderr.putc "." # BN_generate_prime 101 | when 1; $stderr.putc "+" # BN_generate_prime 102 | when 2; $stderr.putc "*" # searching good prime, 103 | # n = #of try, 104 | # but also data from BN_generate_prime 105 | when 3; $stderr.putc "\n" # found good prime, n==0 - p, n==1 - q, 106 | # but also data from BN_generate_prime 107 | else; $stderr.putc "*" # BN_generate_prime 108 | end 109 | } 110 | cert = OpenSSL::X509::Certificate.new 111 | cert.version = 2 112 | cert.serial = 1 113 | name = (cn.kind_of? String) ? OpenSSL::X509::Name.parse(cn) 114 | : OpenSSL::X509::Name.new(cn) 115 | cert.subject = name 116 | cert.issuer = name 117 | cert.not_before = Time.now 118 | cert.not_after = Time.now + (365*24*60*60) 119 | cert.public_key = rsa.public_key 120 | 121 | ef = OpenSSL::X509::ExtensionFactory.new(nil,cert) 122 | ef.issuer_certificate = cert 123 | cert.extensions = [ 124 | ef.create_extension("basicConstraints","CA:FALSE"), 125 | ef.create_extension("keyUsage", "keyEncipherment, digitalSignature, keyAgreement, dataEncipherment"), 126 | ef.create_extension("subjectKeyIdentifier", "hash"), 127 | ef.create_extension("extendedKeyUsage", "serverAuth"), 128 | ef.create_extension("nsComment", comment), 129 | ] 130 | aki = ef.create_extension("authorityKeyIdentifier", 131 | "keyid:always,issuer:always") 132 | cert.add_extension(aki) 133 | cert.sign(rsa, "SHA256") 134 | 135 | return [ cert, rsa ] 136 | end 137 | module_function :create_self_signed_cert 138 | end 139 | 140 | ## 141 | #-- 142 | # Updates WEBrick::GenericServer with SSL functionality 143 | 144 | class GenericServer 145 | 146 | ## 147 | # SSL context for the server when run in SSL mode 148 | 149 | def ssl_context # :nodoc: 150 | @ssl_context ||= begin 151 | if @config[:SSLEnable] 152 | ssl_context = setup_ssl_context(@config) 153 | @logger.info("\n" + @config[:SSLCertificate].to_text) 154 | ssl_context 155 | end 156 | end 157 | end 158 | 159 | undef listen 160 | 161 | ## 162 | # Updates +listen+ to enable SSL when the SSL configuration is active. 163 | 164 | def listen(address, port) # :nodoc: 165 | listeners = Utils::create_listeners(address, port) 166 | if @config[:SSLEnable] 167 | listeners.collect!{|svr| 168 | ssvr = ::OpenSSL::SSL::SSLServer.new(svr, ssl_context) 169 | ssvr.start_immediately = @config[:SSLStartImmediately] 170 | ssvr 171 | } 172 | end 173 | @listeners += listeners 174 | setup_shutdown_pipe 175 | end 176 | 177 | ## 178 | # Sets up an SSL context for +config+ 179 | 180 | def setup_ssl_context(config) # :nodoc: 181 | unless config[:SSLCertificate] 182 | cn = config[:SSLCertName] 183 | comment = config[:SSLCertComment] 184 | cert, key = Utils::create_self_signed_cert(2048, cn, comment) 185 | config[:SSLCertificate] = cert 186 | config[:SSLPrivateKey] = key 187 | end 188 | ctx = OpenSSL::SSL::SSLContext.new 189 | ctx.key = config[:SSLPrivateKey] 190 | ctx.cert = config[:SSLCertificate] 191 | ctx.client_ca = config[:SSLClientCA] 192 | ctx.extra_chain_cert = config[:SSLExtraChainCert] 193 | ctx.ca_file = config[:SSLCACertificateFile] 194 | ctx.ca_path = config[:SSLCACertificatePath] 195 | ctx.cert_store = config[:SSLCertificateStore] 196 | ctx.tmp_dh_callback = config[:SSLTmpDhCallback] 197 | ctx.verify_mode = config[:SSLVerifyClient] 198 | ctx.verify_depth = config[:SSLVerifyDepth] 199 | ctx.verify_callback = config[:SSLVerifyCallback] 200 | ctx.servername_cb = config[:SSLServerNameCallback] || proc { |args| ssl_servername_callback(*args) } 201 | ctx.timeout = config[:SSLTimeout] 202 | ctx.options = config[:SSLOptions] 203 | ctx.ciphers = config[:SSLCiphers] 204 | ctx 205 | end 206 | 207 | ## 208 | # ServerNameIndication callback 209 | 210 | def ssl_servername_callback(sslsocket, hostname = nil) 211 | # default 212 | end 213 | 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /test/webrick/test_httpresponse.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "webrick" 3 | require "stringio" 4 | require "net/http" 5 | 6 | module WEBrick 7 | class TestHTTPResponse < Test::Unit::TestCase 8 | class FakeLogger 9 | attr_reader :messages 10 | 11 | def initialize 12 | @messages = [] 13 | end 14 | 15 | def warn msg 16 | @messages << msg 17 | end 18 | end 19 | 20 | attr_reader :config, :logger, :res 21 | 22 | def setup 23 | super 24 | @logger = FakeLogger.new 25 | @config = Config::HTTP 26 | @config[:Logger] = logger 27 | @res = HTTPResponse.new config 28 | @res.keep_alive = true 29 | end 30 | 31 | def test_prevent_response_splitting_headers_crlf 32 | res['X-header'] = "malicious\r\nCookie: cracked_indicator_for_test" 33 | io = StringIO.new 34 | res.send_response io 35 | io.rewind 36 | res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) 37 | assert_equal '500', res.code 38 | refute_match 'cracked_indicator_for_test', io.string 39 | end 40 | 41 | def test_prevent_response_splitting_cookie_headers_crlf 42 | user_input = "malicious\r\nCookie: cracked_indicator_for_test" 43 | res.cookies << WEBrick::Cookie.new('author', user_input) 44 | io = StringIO.new 45 | res.send_response io 46 | io.rewind 47 | res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) 48 | assert_equal '500', res.code 49 | refute_match 'cracked_indicator_for_test', io.string 50 | end 51 | 52 | def test_prevent_response_splitting_headers_cr 53 | res['X-header'] = "malicious\rCookie: cracked_indicator_for_test" 54 | io = StringIO.new 55 | res.send_response io 56 | io.rewind 57 | res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) 58 | assert_equal '500', res.code 59 | refute_match 'cracked_indicator_for_test', io.string 60 | end 61 | 62 | def test_prevent_response_splitting_cookie_headers_cr 63 | user_input = "malicious\rCookie: cracked_indicator_for_test" 64 | res.cookies << WEBrick::Cookie.new('author', user_input) 65 | io = StringIO.new 66 | res.send_response io 67 | io.rewind 68 | res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) 69 | assert_equal '500', res.code 70 | refute_match 'cracked_indicator_for_test', io.string 71 | end 72 | 73 | def test_prevent_response_splitting_headers_lf 74 | res['X-header'] = "malicious\nCookie: cracked_indicator_for_test" 75 | io = StringIO.new 76 | res.send_response io 77 | io.rewind 78 | res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) 79 | assert_equal '500', res.code 80 | refute_match 'cracked_indicator_for_test', io.string 81 | end 82 | 83 | def test_prevent_response_splitting_cookie_headers_lf 84 | user_input = "malicious\nCookie: cracked_indicator_for_test" 85 | res.cookies << WEBrick::Cookie.new('author', user_input) 86 | io = StringIO.new 87 | res.send_response io 88 | io.rewind 89 | res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) 90 | assert_equal '500', res.code 91 | refute_match 'cracked_indicator_for_test', io.string 92 | end 93 | 94 | def test_set_redirect_response_splitting 95 | url = "malicious\r\nCookie: cracked_indicator_for_test" 96 | assert_raises(URI::InvalidURIError) do 97 | res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) 98 | end 99 | end 100 | 101 | def test_set_redirect_html_injection 102 | url = 'http://example.com////?a' 103 | assert_raises(WEBrick::HTTPStatus::MultipleChoices) do 104 | res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) 105 | end 106 | res.status = 300 107 | io = StringIO.new 108 | res.send_response(io) 109 | io.rewind 110 | res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) 111 | assert_equal '300', res.code 112 | refute_match(/@options instance 54 | # variable for use by a subclass. 55 | 56 | def initialize(*args) 57 | if defined?(MOD_RUBY) 58 | unless ENV.has_key?("GATEWAY_INTERFACE") 59 | Apache.request.setup_cgi_env 60 | end 61 | end 62 | if %r{HTTP/(\d+\.\d+)} =~ ENV["SERVER_PROTOCOL"] 63 | httpv = $1 64 | end 65 | @config = WEBrick::Config::HTTP.dup.update( 66 | :ServerSoftware => ENV["SERVER_SOFTWARE"] || "null", 67 | :HTTPVersion => HTTPVersion.new(httpv || "1.0"), 68 | :RunOnCGI => true, # to detect if it runs on CGI. 69 | :NPH => false # set true to run as NPH script. 70 | ) 71 | if config = args.shift 72 | @config.update(config) 73 | end 74 | @config[:Logger] ||= WEBrick::BasicLog.new($stderr) 75 | @logger = @config[:Logger] 76 | @options = args 77 | end 78 | 79 | ## 80 | # Reads +key+ from the configuration 81 | 82 | def [](key) 83 | @config[key] 84 | end 85 | 86 | ## 87 | # Starts the CGI process with the given environment +env+ and standard 88 | # input and output +stdin+ and +stdout+. 89 | 90 | def start(env=ENV, stdin=$stdin, stdout=$stdout) 91 | sock = WEBrick::CGI::Socket.new(@config, env, stdin, stdout) 92 | req = HTTPRequest.new(@config) 93 | res = HTTPResponse.new(@config) 94 | unless @config[:NPH] or defined?(MOD_RUBY) 95 | def res.setup_header 96 | unless @header["status"] 97 | phrase = HTTPStatus::reason_phrase(@status) 98 | @header["status"] = "#{@status} #{phrase}" 99 | end 100 | super 101 | end 102 | def res.status_line 103 | "" 104 | end 105 | end 106 | 107 | begin 108 | req.parse(sock) 109 | req.script_name = (env["SCRIPT_NAME"] || File.expand_path($0)).dup 110 | req.path_info = (env["PATH_INFO"] || "").dup 111 | req.query_string = env["QUERY_STRING"] 112 | req.user = env["REMOTE_USER"] 113 | res.request_method = req.request_method 114 | res.request_uri = req.request_uri 115 | res.request_http_version = req.http_version 116 | res.keep_alive = req.keep_alive? 117 | self.service(req, res) 118 | rescue HTTPStatus::Error => ex 119 | res.set_error(ex) 120 | rescue HTTPStatus::Status => ex 121 | res.status = ex.code 122 | rescue Exception => ex 123 | @logger.error(ex) 124 | res.set_error(ex, true) 125 | ensure 126 | req.fixup 127 | if defined?(MOD_RUBY) 128 | res.setup_header 129 | Apache.request.status_line = "#{res.status} #{res.reason_phrase}" 130 | Apache.request.status = res.status 131 | table = Apache.request.headers_out 132 | res.header.each{|key, val| 133 | case key 134 | when /^content-encoding$/i 135 | Apache::request.content_encoding = val 136 | when /^content-type$/i 137 | Apache::request.content_type = val 138 | else 139 | table[key] = val.to_s 140 | end 141 | } 142 | res.cookies.each{|cookie| 143 | table.add("Set-Cookie", cookie.to_s) 144 | } 145 | Apache.request.send_http_header 146 | res.send_body(sock) 147 | else 148 | res.send_response(sock) 149 | end 150 | end 151 | end 152 | 153 | ## 154 | # Services the request +req+ which will fill in the response +res+. See 155 | # WEBrick::HTTPServlet::AbstractServlet#service for details. 156 | 157 | def service(req, res) 158 | method_name = "do_" + req.request_method.gsub(/-/, "_") 159 | if respond_to?(method_name) 160 | __send__(method_name, req, res) 161 | else 162 | raise HTTPStatus::MethodNotAllowed, 163 | "unsupported method `#{req.request_method}'." 164 | end 165 | end 166 | 167 | ## 168 | # Provides HTTP socket emulation from the CGI environment 169 | 170 | class Socket # :nodoc: 171 | include Enumerable 172 | 173 | private 174 | 175 | def initialize(config, env, stdin, stdout) 176 | @config = config 177 | @env = env 178 | @header_part = StringIO.new 179 | @body_part = stdin 180 | @out_port = stdout 181 | @out_port.binmode 182 | 183 | @server_addr = @env["SERVER_ADDR"] || "0.0.0.0" 184 | @server_name = @env["SERVER_NAME"] 185 | @server_port = @env["SERVER_PORT"] 186 | @remote_addr = @env["REMOTE_ADDR"] 187 | @remote_host = @env["REMOTE_HOST"] || @remote_addr 188 | @remote_port = @env["REMOTE_PORT"] || 0 189 | 190 | begin 191 | @header_part << request_line << CRLF 192 | setup_header 193 | @header_part << CRLF 194 | @header_part.rewind 195 | rescue Exception 196 | raise CGIError, "invalid CGI environment" 197 | end 198 | end 199 | 200 | def request_line 201 | meth = @env["REQUEST_METHOD"] || "GET" 202 | unless url = @env["REQUEST_URI"] 203 | url = (@env["SCRIPT_NAME"] || File.expand_path($0)).dup 204 | url << @env["PATH_INFO"].to_s 205 | url = WEBrick::HTTPUtils.escape_path(url) 206 | if query_string = @env["QUERY_STRING"] 207 | unless query_string.empty? 208 | url << "?" << query_string 209 | end 210 | end 211 | end 212 | # we cannot get real HTTP version of client ;) 213 | httpv = @config[:HTTPVersion] 214 | return "#{meth} #{url} HTTP/#{httpv}" 215 | end 216 | 217 | def setup_header 218 | @env.each{|key, value| 219 | case key 220 | when "CONTENT_TYPE", "CONTENT_LENGTH" 221 | add_header(key.gsub(/_/, "-"), value) 222 | when /^HTTP_(.*)/ 223 | add_header($1.gsub(/_/, "-"), value) 224 | end 225 | } 226 | end 227 | 228 | def add_header(hdrname, value) 229 | unless value.empty? 230 | @header_part << hdrname << ": " << value << CRLF 231 | end 232 | end 233 | 234 | def input 235 | @header_part.eof? ? @body_part : @header_part 236 | end 237 | 238 | public 239 | 240 | def peeraddr 241 | [nil, @remote_port, @remote_host, @remote_addr] 242 | end 243 | 244 | def addr 245 | [nil, @server_port, @server_name, @server_addr] 246 | end 247 | 248 | def gets(eol=LF, size=nil) 249 | input.gets(eol, size) 250 | end 251 | 252 | def read(size=nil) 253 | input.read(size) 254 | end 255 | 256 | def each 257 | input.each{|line| yield(line) } 258 | end 259 | 260 | def eof? 261 | input.eof? 262 | end 263 | 264 | def <<(data) 265 | @out_port << data 266 | end 267 | 268 | def write(data) 269 | @out_port.write(data) 270 | end 271 | 272 | def cert 273 | return nil unless defined?(OpenSSL) 274 | if pem = @env["SSL_SERVER_CERT"] 275 | OpenSSL::X509::Certificate.new(pem) unless pem.empty? 276 | end 277 | end 278 | 279 | def peer_cert 280 | return nil unless defined?(OpenSSL) 281 | if pem = @env["SSL_CLIENT_CERT"] 282 | OpenSSL::X509::Certificate.new(pem) unless pem.empty? 283 | end 284 | end 285 | 286 | def peer_cert_chain 287 | return nil unless defined?(OpenSSL) 288 | if @env["SSL_CLIENT_CERT_CHAIN_0"] 289 | keys = @env.keys 290 | certs = keys.sort.collect{|k| 291 | if /^SSL_CLIENT_CERT_CHAIN_\d+$/ =~ k 292 | if pem = @env[k] 293 | OpenSSL::X509::Certificate.new(pem) unless pem.empty? 294 | end 295 | end 296 | } 297 | certs.compact 298 | end 299 | end 300 | 301 | def cipher 302 | return nil unless defined?(OpenSSL) 303 | if cipher = @env["SSL_CIPHER"] 304 | ret = [ cipher ] 305 | ret << @env["SSL_PROTOCOL"] 306 | ret << @env["SSL_CIPHER_USEKEYSIZE"] 307 | ret << @env["SSL_CIPHER_ALGKEYSIZE"] 308 | ret 309 | end 310 | end 311 | end 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /lib/webrick/httpserver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpserver.rb -- HTTPServer Class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $ 11 | 12 | require 'io/wait' 13 | require_relative 'server' 14 | require_relative 'httputils' 15 | require_relative 'httpstatus' 16 | require_relative 'httprequest' 17 | require_relative 'httpresponse' 18 | require_relative 'httpservlet' 19 | require_relative 'accesslog' 20 | 21 | module WEBrick 22 | class HTTPServerError < ServerError; end 23 | 24 | ## 25 | # An HTTP Server 26 | 27 | class HTTPServer < ::WEBrick::GenericServer 28 | ## 29 | # Creates a new HTTP server according to +config+ 30 | # 31 | # An HTTP server uses the following attributes: 32 | # 33 | # :AccessLog:: An array of access logs. See WEBrick::AccessLog 34 | # :BindAddress:: Local address for the server to bind to 35 | # :DocumentRoot:: Root path to serve files from 36 | # :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler 37 | # :HTTPVersion:: The HTTP version of this server 38 | # :Port:: Port to listen on 39 | # :RequestCallback:: Called with a request and response before each 40 | # request is serviced. 41 | # :RequestTimeout:: Maximum time to wait between requests 42 | # :ServerAlias:: Array of alternate names for this server for virtual 43 | # hosting 44 | # :ServerName:: Name for this server for virtual hosting 45 | 46 | def initialize(config={}, default=Config::HTTP) 47 | super(config, default) 48 | @http_version = HTTPVersion::convert(@config[:HTTPVersion]) 49 | 50 | @mount_tab = MountTable.new 51 | if @config[:DocumentRoot] 52 | mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot], 53 | @config[:DocumentRootOptions]) 54 | end 55 | 56 | unless @config[:AccessLog] 57 | @config[:AccessLog] = [ 58 | [ $stderr, AccessLog::COMMON_LOG_FORMAT ], 59 | [ $stderr, AccessLog::REFERER_LOG_FORMAT ] 60 | ] 61 | end 62 | 63 | @virtual_hosts = Array.new 64 | end 65 | 66 | ## 67 | # Processes requests on +sock+ 68 | 69 | def run(sock) 70 | while true 71 | req = create_request(@config) 72 | res = create_response(@config) 73 | server = self 74 | begin 75 | timeout = @config[:RequestTimeout] 76 | while timeout > 0 77 | break if sock.to_io.wait_readable(0.5) 78 | break if @status != :Running 79 | timeout -= 0.5 80 | end 81 | raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running 82 | raise HTTPStatus::EOFError if sock.eof? 83 | req.parse(sock) 84 | res.request_method = req.request_method 85 | res.request_uri = req.request_uri 86 | res.request_http_version = req.http_version 87 | res.keep_alive = req.keep_alive? 88 | server = lookup_server(req) || self 89 | if callback = server[:RequestCallback] 90 | callback.call(req, res) 91 | elsif callback = server[:RequestHandler] 92 | msg = ":RequestHandler is deprecated, please use :RequestCallback" 93 | @logger.warn(msg) 94 | callback.call(req, res) 95 | end 96 | server.service(req, res) 97 | rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex 98 | res.set_error(ex) 99 | rescue HTTPStatus::Error => ex 100 | @logger.error(ex.message) 101 | res.set_error(ex) 102 | rescue HTTPStatus::Status => ex 103 | res.status = ex.code 104 | rescue StandardError => ex 105 | @logger.error(ex) 106 | res.set_error(ex, true) 107 | ensure 108 | if req.request_line 109 | if req.keep_alive? && res.keep_alive? 110 | req.fixup() 111 | end 112 | res.send_response(sock) 113 | server.access_log(@config, req, res) 114 | end 115 | end 116 | break if @http_version < "1.1" 117 | break unless req.keep_alive? 118 | break unless res.keep_alive? 119 | end 120 | end 121 | 122 | ## 123 | # Services +req+ and fills in +res+ 124 | 125 | def service(req, res) 126 | if req.unparsed_uri == "*" 127 | if req.request_method == "OPTIONS" 128 | do_OPTIONS(req, res) 129 | raise HTTPStatus::OK 130 | end 131 | raise HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found." 132 | end 133 | 134 | servlet, options, script_name, path_info = search_servlet(req.path) 135 | raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet 136 | req.script_name = script_name 137 | req.path_info = path_info 138 | si = servlet.get_instance(self, *options) 139 | @logger.debug(format("%s is invoked.", si.class.name)) 140 | si.service(req, res) 141 | end 142 | 143 | ## 144 | # The default OPTIONS request handler says GET, HEAD, POST and OPTIONS 145 | # requests are allowed. 146 | 147 | def do_OPTIONS(req, res) 148 | res["allow"] = "GET,HEAD,POST,OPTIONS" 149 | end 150 | 151 | ## 152 | # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation 153 | # time 154 | 155 | def mount(dir, servlet, *options) 156 | @logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir)) 157 | @mount_tab[dir] = [ servlet, options ] 158 | end 159 | 160 | ## 161 | # Mounts +proc+ or +block+ on +dir+ and calls it with a 162 | # WEBrick::HTTPRequest and WEBrick::HTTPResponse 163 | 164 | def mount_proc(dir, proc=nil, &block) 165 | proc ||= block 166 | raise HTTPServerError, "must pass a proc or block" unless proc 167 | mount(dir, HTTPServlet::ProcHandler.new(proc)) 168 | end 169 | 170 | ## 171 | # Unmounts +dir+ 172 | 173 | def unmount(dir) 174 | @logger.debug(sprintf("unmount %s.", dir)) 175 | @mount_tab.delete(dir) 176 | end 177 | alias umount unmount 178 | 179 | ## 180 | # Finds a servlet for +path+ 181 | 182 | def search_servlet(path) 183 | script_name, path_info = @mount_tab.scan(path) 184 | servlet, options = @mount_tab[script_name] 185 | if servlet 186 | [ servlet, options, script_name, path_info ] 187 | end 188 | end 189 | 190 | ## 191 | # Adds +server+ as a virtual host. 192 | 193 | def virtual_host(server) 194 | @virtual_hosts << server 195 | @virtual_hosts = @virtual_hosts.sort_by{|s| 196 | num = 0 197 | num -= 4 if s[:BindAddress] 198 | num -= 2 if s[:Port] 199 | num -= 1 if s[:ServerName] 200 | num 201 | } 202 | end 203 | 204 | ## 205 | # Finds the appropriate virtual host to handle +req+ 206 | 207 | def lookup_server(req) 208 | @virtual_hosts.find{|s| 209 | (s[:BindAddress].nil? || req.addr[3] == s[:BindAddress]) && 210 | (s[:Port].nil? || req.port == s[:Port]) && 211 | ((s[:ServerName].nil? || req.host == s[:ServerName]) || 212 | (!s[:ServerAlias].nil? && s[:ServerAlias].find{|h| h === req.host})) 213 | } 214 | end 215 | 216 | ## 217 | # Logs +req+ and +res+ in the access logs. +config+ is used for the 218 | # server name. 219 | 220 | def access_log(config, req, res) 221 | param = AccessLog::setup_params(config, req, res) 222 | @config[:AccessLog].each{|logger, fmt| 223 | logger << AccessLog::format(fmt+"\n", param) 224 | } 225 | end 226 | 227 | ## 228 | # Creates the HTTPRequest used when handling the HTTP 229 | # request. Can be overridden by subclasses. 230 | def create_request(with_webrick_config) 231 | HTTPRequest.new(with_webrick_config) 232 | end 233 | 234 | ## 235 | # Creates the HTTPResponse used when handling the HTTP 236 | # request. Can be overridden by subclasses. 237 | def create_response(with_webrick_config) 238 | HTTPResponse.new(with_webrick_config) 239 | end 240 | 241 | ## 242 | # Mount table for the path a servlet is mounted on in the directory space 243 | # of the server. Users of WEBrick can only access this indirectly via 244 | # WEBrick::HTTPServer#mount, WEBrick::HTTPServer#unmount and 245 | # WEBrick::HTTPServer#search_servlet 246 | 247 | class MountTable # :nodoc: 248 | def initialize 249 | @tab = Hash.new 250 | compile 251 | end 252 | 253 | def [](dir) 254 | dir = normalize(dir) 255 | @tab[dir] 256 | end 257 | 258 | def []=(dir, val) 259 | dir = normalize(dir) 260 | @tab[dir] = val 261 | compile 262 | val 263 | end 264 | 265 | def delete(dir) 266 | dir = normalize(dir) 267 | res = @tab.delete(dir) 268 | compile 269 | res 270 | end 271 | 272 | def scan(path) 273 | @scanner =~ path 274 | [ $&, $' ] 275 | end 276 | 277 | private 278 | 279 | def compile 280 | k = @tab.keys 281 | k.sort! 282 | k.reverse! 283 | k.collect!{|path| Regexp.escape(path) } 284 | @scanner = Regexp.new("\\A(" + k.join("|") +")(?=/|\\z)") 285 | end 286 | 287 | def normalize(dir) 288 | ret = dir ? dir.dup : +"" 289 | ret.sub!(%r|/+\z|, "") 290 | ret 291 | end 292 | end 293 | end 294 | end 295 | -------------------------------------------------------------------------------- /test/lib/envutil.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: us-ascii -*- 2 | # frozen_string_literal: true 3 | require "open3" 4 | require "timeout" 5 | require_relative "find_executable" 6 | begin 7 | require 'rbconfig' 8 | rescue LoadError 9 | end 10 | begin 11 | require "rbconfig/sizeof" 12 | rescue LoadError 13 | end 14 | 15 | module EnvUtil 16 | def rubybin 17 | if ruby = ENV["RUBY"] 18 | return ruby 19 | end 20 | ruby = "ruby" 21 | exeext = RbConfig::CONFIG["EXEEXT"] 22 | rubyexe = (ruby + exeext if exeext and !exeext.empty?) 23 | 3.times do 24 | if File.exist? ruby and File.executable? ruby and !File.directory? ruby 25 | return File.expand_path(ruby) 26 | end 27 | if rubyexe and File.exist? rubyexe and File.executable? rubyexe 28 | return File.expand_path(rubyexe) 29 | end 30 | ruby = File.join("..", ruby) 31 | end 32 | if defined?(RbConfig.ruby) 33 | RbConfig.ruby 34 | else 35 | "ruby" 36 | end 37 | end 38 | module_function :rubybin 39 | 40 | LANG_ENVS = %w"LANG LC_ALL LC_CTYPE" 41 | 42 | DEFAULT_SIGNALS = Signal.list 43 | DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM 44 | 45 | RUBYLIB = ENV["RUBYLIB"] 46 | 47 | class << self 48 | attr_accessor :timeout_scale 49 | attr_reader :original_internal_encoding, :original_external_encoding, 50 | :original_verbose, :original_warning 51 | 52 | def capture_global_values 53 | @original_internal_encoding = Encoding.default_internal 54 | @original_external_encoding = Encoding.default_external 55 | @original_verbose = $VERBOSE 56 | @original_warning = defined?(Warning.[]) ? %i[deprecated experimental].to_h {|i| [i, Warning[i]]} : nil 57 | end 58 | end 59 | 60 | def apply_timeout_scale(t) 61 | if scale = EnvUtil.timeout_scale 62 | t * scale 63 | else 64 | t 65 | end 66 | end 67 | module_function :apply_timeout_scale 68 | 69 | def timeout(sec, klass = nil, message = nil, &blk) 70 | return yield(sec) if sec == nil or sec.zero? 71 | sec = apply_timeout_scale(sec) 72 | Timeout.timeout(sec, klass, message, &blk) 73 | end 74 | module_function :timeout 75 | 76 | def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) 77 | reprieve = apply_timeout_scale(reprieve) if reprieve 78 | 79 | signals = Array(signal).select do |sig| 80 | DEFAULT_SIGNALS[sig.to_s] or 81 | DEFAULT_SIGNALS[Signal.signame(sig)] rescue false 82 | end 83 | signals |= [:ABRT, :KILL] 84 | case pgroup 85 | when 0, true 86 | pgroup = -pid 87 | when nil, false 88 | pgroup = pid 89 | end 90 | 91 | lldb = true if /darwin/ =~ RUBY_PLATFORM 92 | 93 | while signal = signals.shift 94 | 95 | if lldb and [:ABRT, :KILL].include?(signal) 96 | lldb = false 97 | # sudo -n: --non-interactive 98 | # lldb -p: attach 99 | # -o: run command 100 | system(*%W[sudo -n lldb -p #{pid} --batch -o bt\ all -o call\ rb_vmdebug_stack_dump_all_threads() -o quit]) 101 | true 102 | end 103 | 104 | begin 105 | Process.kill signal, pgroup 106 | rescue Errno::EINVAL 107 | next 108 | rescue Errno::ESRCH 109 | break 110 | end 111 | if signals.empty? or !reprieve 112 | Process.wait(pid) 113 | else 114 | begin 115 | Timeout.timeout(reprieve) {Process.wait(pid)} 116 | rescue Timeout::Error 117 | else 118 | break 119 | end 120 | end 121 | end 122 | $? 123 | end 124 | module_function :terminate 125 | 126 | def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false, 127 | encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error, 128 | stdout_filter: nil, stderr_filter: nil, ios: nil, 129 | signal: :TERM, 130 | rubybin: EnvUtil.rubybin, precommand: nil, 131 | **opt) 132 | timeout = apply_timeout_scale(timeout) 133 | 134 | in_c, in_p = IO.pipe 135 | out_p, out_c = IO.pipe if capture_stdout 136 | err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout 137 | opt[:in] = in_c 138 | opt[:out] = out_c if capture_stdout 139 | opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr 140 | if encoding 141 | out_p.set_encoding(encoding) if out_p 142 | err_p.set_encoding(encoding) if err_p 143 | end 144 | ios.each {|i, o = i|opt[i] = o} if ios 145 | 146 | c = "C" 147 | child_env = {} 148 | LANG_ENVS.each {|lc| child_env[lc] = c} 149 | if Array === args and Hash === args.first 150 | child_env.update(args.shift) 151 | end 152 | if RUBYLIB and lib = child_env["RUBYLIB"] 153 | child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR) 154 | end 155 | child_env['ASAN_OPTIONS'] = ENV['ASAN_OPTIONS'] if ENV['ASAN_OPTIONS'] 156 | args = [args] if args.kind_of?(String) 157 | pid = spawn(child_env, *precommand, rubybin, *args, opt) 158 | in_c.close 159 | out_c&.close 160 | out_c = nil 161 | err_c&.close 162 | err_c = nil 163 | if block_given? 164 | return yield in_p, out_p, err_p, pid 165 | else 166 | th_stdout = Thread.new { out_p.read } if capture_stdout 167 | th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout 168 | in_p.write stdin_data.to_str unless stdin_data.empty? 169 | in_p.close 170 | if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) 171 | timeout_error = nil 172 | else 173 | status = terminate(pid, signal, opt[:pgroup], reprieve) 174 | terminated = Time.now 175 | end 176 | stdout = th_stdout.value if capture_stdout 177 | stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout 178 | out_p.close if capture_stdout 179 | err_p.close if capture_stderr && capture_stderr != :merge_to_stdout 180 | status ||= Process.wait2(pid)[1] 181 | stdout = stdout_filter.call(stdout) if stdout_filter 182 | stderr = stderr_filter.call(stderr) if stderr_filter 183 | if timeout_error 184 | bt = caller_locations 185 | msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)" 186 | msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n")) 187 | raise timeout_error, msg, bt.map(&:to_s) 188 | end 189 | return stdout, stderr, status 190 | end 191 | ensure 192 | [th_stdout, th_stderr].each do |th| 193 | th.kill if th 194 | end 195 | [in_c, in_p, out_c, out_p, err_c, err_p].each do |io| 196 | io&.close 197 | end 198 | [th_stdout, th_stderr].each do |th| 199 | th.join if th 200 | end 201 | end 202 | module_function :invoke_ruby 203 | 204 | def verbose_warning 205 | class << (stderr = "".dup) 206 | alias write concat 207 | def flush; end 208 | end 209 | stderr, $stderr = $stderr, stderr 210 | $VERBOSE = true 211 | yield stderr 212 | return $stderr 213 | ensure 214 | stderr, $stderr = $stderr, stderr 215 | $VERBOSE = EnvUtil.original_verbose 216 | EnvUtil.original_warning&.each {|i, v| Warning[i] = v} 217 | end 218 | module_function :verbose_warning 219 | 220 | def default_warning 221 | $VERBOSE = false 222 | yield 223 | ensure 224 | $VERBOSE = EnvUtil.original_verbose 225 | end 226 | module_function :default_warning 227 | 228 | def suppress_warning 229 | $VERBOSE = nil 230 | yield 231 | ensure 232 | $VERBOSE = EnvUtil.original_verbose 233 | end 234 | module_function :suppress_warning 235 | 236 | def under_gc_stress(stress = true) 237 | stress, GC.stress = GC.stress, stress 238 | yield 239 | ensure 240 | GC.stress = stress 241 | end 242 | module_function :under_gc_stress 243 | 244 | def with_default_external(enc) 245 | suppress_warning { Encoding.default_external = enc } 246 | yield 247 | ensure 248 | suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding } 249 | end 250 | module_function :with_default_external 251 | 252 | def with_default_internal(enc) 253 | suppress_warning { Encoding.default_internal = enc } 254 | yield 255 | ensure 256 | suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding } 257 | end 258 | module_function :with_default_internal 259 | 260 | def labeled_module(name, &block) 261 | Module.new do 262 | singleton_class.class_eval { 263 | define_method(:to_s) {name} 264 | alias inspect to_s 265 | alias name to_s 266 | } 267 | class_eval(&block) if block 268 | end 269 | end 270 | module_function :labeled_module 271 | 272 | def labeled_class(name, superclass = Object, &block) 273 | Class.new(superclass) do 274 | singleton_class.class_eval { 275 | define_method(:to_s) {name} 276 | alias inspect to_s 277 | alias name to_s 278 | } 279 | class_eval(&block) if block 280 | end 281 | end 282 | module_function :labeled_class 283 | 284 | if /darwin/ =~ RUBY_PLATFORM 285 | DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports") 286 | DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S' 287 | @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME'] 288 | 289 | def self.diagnostic_reports(signame, pid, now) 290 | return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame) 291 | cmd = File.basename(rubybin) 292 | cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd 293 | path = DIAGNOSTIC_REPORTS_PATH 294 | timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT 295 | pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash" 296 | first = true 297 | 30.times do 298 | first ? (first = false) : sleep(0.1) 299 | Dir.glob(pat) do |name| 300 | log = File.read(name) rescue next 301 | if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log 302 | File.unlink(name) 303 | File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil 304 | return log 305 | end 306 | end 307 | end 308 | nil 309 | end 310 | else 311 | def self.diagnostic_reports(signame, pid, now) 312 | end 313 | end 314 | 315 | def self.failure_description(status, now, message = "", out = "") 316 | pid = status.pid 317 | if signo = status.termsig 318 | signame = Signal.signame(signo) 319 | sigdesc = "signal #{signo}" 320 | end 321 | log = diagnostic_reports(signame, pid, now) 322 | if signame 323 | sigdesc = "SIG#{signame} (#{sigdesc})" 324 | end 325 | if status.coredump? 326 | sigdesc = "#{sigdesc} (core dumped)" 327 | end 328 | full_message = ''.dup 329 | message = message.call if Proc === message 330 | if message and !message.empty? 331 | full_message << message << "\n" 332 | end 333 | full_message << "pid #{pid}" 334 | full_message << " exit #{status.exitstatus}" if status.exited? 335 | full_message << " killed by #{sigdesc}" if sigdesc 336 | if out and !out.empty? 337 | full_message << "\n" << out.b.gsub(/^/, '| ') 338 | full_message.sub!(/(?:MaxClients configuration sets this. 76 | 77 | attr_reader :tokens 78 | 79 | ## 80 | # Sockets listening for connections. 81 | 82 | attr_reader :listeners 83 | 84 | ## 85 | # Creates a new generic server from +config+. The default configuration 86 | # comes from +default+. 87 | 88 | def initialize(config={}, default=Config::General) 89 | @config = default.dup.update(config) 90 | @status = :Stop 91 | @config[:Logger] ||= Log::new 92 | @logger = @config[:Logger] 93 | 94 | @tokens = Thread::SizedQueue.new(@config[:MaxClients]) 95 | @config[:MaxClients].times{ @tokens.push(nil) } 96 | 97 | webrickv = WEBrick::VERSION 98 | rubyv = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" 99 | @logger.info("WEBrick #{webrickv}") 100 | @logger.info("ruby #{rubyv}") 101 | 102 | @listeners = [] 103 | @shutdown_pipe = nil 104 | unless @config[:DoNotListen] 105 | raise ArgumentError, "Port must be an integer" unless @config[:Port].to_s == @config[:Port].to_i.to_s 106 | 107 | @config[:Port] = @config[:Port].to_i 108 | if @config[:Listen] 109 | warn(":Listen option is deprecated; use GenericServer#listen", uplevel: 1) 110 | end 111 | listen(@config[:BindAddress], @config[:Port]) 112 | if @config[:Port] == 0 113 | @config[:Port] = @listeners[0].addr[1] 114 | end 115 | end 116 | end 117 | 118 | ## 119 | # Retrieves +key+ from the configuration 120 | 121 | def [](key) 122 | @config[key] 123 | end 124 | 125 | ## 126 | # Adds listeners from +address+ and +port+ to the server. See 127 | # WEBrick::Utils::create_listeners for details. 128 | 129 | def listen(address, port) 130 | @listeners += Utils::create_listeners(address, port) 131 | end 132 | 133 | ## 134 | # Starts the server and runs the +block+ for each connection. This method 135 | # does not return until the server is stopped from a signal handler or 136 | # another thread using #stop or #shutdown. 137 | # 138 | # If the block raises a subclass of StandardError the exception is logged 139 | # and ignored. If an IOError or Errno::EBADF exception is raised the 140 | # exception is ignored. If an Exception subclass is raised the exception 141 | # is logged and re-raised which stops the server. 142 | # 143 | # To completely shut down a server call #shutdown from ensure: 144 | # 145 | # server = WEBrick::GenericServer.new 146 | # # or WEBrick::HTTPServer.new 147 | # 148 | # begin 149 | # server.start 150 | # ensure 151 | # server.shutdown 152 | # end 153 | 154 | def start(&block) 155 | raise ServerError, "already started." if @status != :Stop 156 | server_type = @config[:ServerType] || SimpleServer 157 | 158 | setup_shutdown_pipe 159 | 160 | server_type.start{ 161 | @logger.info \ 162 | "#{self.class}#start: pid=#{$$} port=#{@config[:Port]}" 163 | @status = :Running 164 | call_callback(:StartCallback) 165 | 166 | shutdown_pipe = @shutdown_pipe 167 | 168 | thgroup = ThreadGroup.new 169 | begin 170 | while @status == :Running 171 | begin 172 | sp = shutdown_pipe[0] 173 | if svrs = IO.select([sp, *@listeners]) 174 | if svrs[0].include? sp 175 | # swallow shutdown pipe 176 | buf = String.new 177 | nil while String === 178 | sp.read_nonblock([sp.nread, 8].max, buf, exception: false) 179 | break 180 | end 181 | svrs[0].each{|svr| 182 | @tokens.pop # blocks while no token is there. 183 | if sock = accept_client(svr) 184 | unless config[:DoNotReverseLookup].nil? 185 | sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup] 186 | end 187 | th = start_thread(sock, &block) 188 | th[:WEBrickThread] = true 189 | thgroup.add(th) 190 | else 191 | @tokens.push(nil) 192 | end 193 | } 194 | end 195 | rescue Errno::EBADF, Errno::ENOTSOCK, IOError => ex 196 | # if the listening socket was closed in GenericServer#shutdown, 197 | # IO::select raise it. 198 | rescue StandardError => ex 199 | msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" 200 | @logger.error msg 201 | rescue Exception => ex 202 | @logger.fatal ex 203 | raise 204 | end 205 | end 206 | ensure 207 | cleanup_shutdown_pipe(shutdown_pipe) 208 | cleanup_listener 209 | @status = :Shutdown 210 | @logger.info "going to shutdown ..." 211 | thgroup.list.each{|th| th.join if th[:WEBrickThread] } 212 | call_callback(:StopCallback) 213 | @logger.info "#{self.class}#start done." 214 | @status = :Stop 215 | end 216 | } 217 | end 218 | 219 | ## 220 | # Stops the server from accepting new connections. 221 | 222 | def stop 223 | if @status == :Running 224 | @status = :Shutdown 225 | end 226 | 227 | alarm_shutdown_pipe {|f| f.write_nonblock("\0")} 228 | end 229 | 230 | ## 231 | # Shuts down the server and all listening sockets. New listeners must be 232 | # provided to restart the server. 233 | 234 | def shutdown 235 | stop 236 | 237 | alarm_shutdown_pipe(&:close) 238 | end 239 | 240 | ## 241 | # You must subclass GenericServer and implement \#run which accepts a TCP 242 | # client socket 243 | 244 | def run(sock) 245 | @logger.fatal "run() must be provided by user." 246 | end 247 | 248 | private 249 | 250 | # :stopdoc: 251 | 252 | ## 253 | # Accepts a TCP client socket from the TCP server socket +svr+ and returns 254 | # the client socket. 255 | 256 | def accept_client(svr) 257 | case sock = svr.to_io.accept_nonblock(exception: false) 258 | when :wait_readable 259 | nil 260 | else 261 | if svr.respond_to?(:start_immediately) 262 | sock = OpenSSL::SSL::SSLSocket.new(sock, ssl_context) 263 | sock.sync_close = true 264 | # we cannot do OpenSSL::SSL::SSLSocket#accept here because 265 | # a slow client can prevent us from accepting connections 266 | # from other clients 267 | end 268 | sock 269 | end 270 | rescue Errno::ECONNRESET, Errno::ECONNABORTED, 271 | Errno::EPROTO, Errno::EINVAL 272 | nil 273 | rescue StandardError => ex 274 | msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" 275 | @logger.error msg 276 | nil 277 | end 278 | 279 | ## 280 | # Starts a server thread for the client socket +sock+ that runs the given 281 | # +block+. 282 | # 283 | # Sets the socket to the :WEBrickSocket thread local variable 284 | # in the thread. 285 | # 286 | # If any errors occur in the block they are logged and handled. 287 | 288 | def start_thread(sock, &block) 289 | Thread.start{ 290 | begin 291 | Thread.current[:WEBrickSocket] = sock 292 | begin 293 | addr = sock.peeraddr 294 | @logger.debug "accept: #{addr[3]}:#{addr[1]}" 295 | rescue SocketError 296 | @logger.debug "accept:

" 297 | raise 298 | end 299 | if sock.respond_to?(:sync_close=) && @config[:SSLStartImmediately] 300 | WEBrick::Utils.timeout(@config[:RequestTimeout]) do 301 | begin 302 | sock.accept # OpenSSL::SSL::SSLSocket#accept 303 | rescue Errno::ECONNRESET, Errno::ECONNABORTED, 304 | Errno::EPROTO, Errno::EINVAL 305 | Thread.exit 306 | end 307 | end 308 | end 309 | call_callback(:AcceptCallback, sock) 310 | block ? block.call(sock) : run(sock) 311 | rescue Errno::ENOTCONN 312 | @logger.debug "Errno::ENOTCONN raised" 313 | rescue ServerError => ex 314 | msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" 315 | @logger.error msg 316 | rescue Exception => ex 317 | @logger.error ex 318 | ensure 319 | @tokens.push(nil) 320 | Thread.current[:WEBrickSocket] = nil 321 | if addr 322 | @logger.debug "close: #{addr[3]}:#{addr[1]}" 323 | else 324 | @logger.debug "close:
" 325 | end 326 | sock.close 327 | end 328 | } 329 | end 330 | 331 | ## 332 | # Calls the callback +callback_name+ from the configuration with +args+ 333 | 334 | def call_callback(callback_name, *args) 335 | @config[callback_name]&.call(*args) 336 | end 337 | 338 | def setup_shutdown_pipe 339 | return @shutdown_pipe ||= IO.pipe 340 | end 341 | 342 | def cleanup_shutdown_pipe(shutdown_pipe) 343 | @shutdown_pipe = nil 344 | shutdown_pipe&.each(&:close) 345 | end 346 | 347 | def alarm_shutdown_pipe 348 | _, pipe = @shutdown_pipe # another thread may modify @shutdown_pipe. 349 | if pipe 350 | if !pipe.closed? 351 | begin 352 | yield pipe 353 | rescue IOError # closed by another thread. 354 | end 355 | end 356 | end 357 | end 358 | 359 | def cleanup_listener 360 | @listeners.each{|s| 361 | if @logger.debug? 362 | addr = s.addr 363 | @logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})") 364 | end 365 | begin 366 | s.shutdown 367 | rescue Errno::ENOTCONN 368 | # when `Errno::ENOTCONN: Socket is not connected' on some platforms, 369 | # call #close instead of #shutdown. 370 | # (ignore @config[:ShutdownSocketWithoutClose]) 371 | s.close 372 | else 373 | unless @config[:ShutdownSocketWithoutClose] 374 | s.close 375 | end 376 | end 377 | } 378 | @listeners.clear 379 | end 380 | end # end of GenericServer 381 | end 382 | -------------------------------------------------------------------------------- /lib/webrick/httpproxy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # httpproxy.rb -- HTTPProxy Class 4 | # 5 | # Author: IPR -- Internet Programming with Ruby -- writers 6 | # Copyright (c) 2002 GOTO Kentaro 7 | # Copyright (c) 2002 Internet Programming with Ruby writers. All rights 8 | # reserved. 9 | # 10 | # $IPR: httpproxy.rb,v 1.18 2003/03/08 18:58:10 gotoyuzo Exp $ 11 | # $kNotwork: straw.rb,v 1.3 2002/02/12 15:13:07 gotoken Exp $ 12 | 13 | require_relative "httpserver" 14 | require "net/http" 15 | 16 | module WEBrick 17 | 18 | NullReader = Object.new # :nodoc: 19 | class << NullReader # :nodoc: 20 | def read(*args) 21 | nil 22 | end 23 | alias gets read 24 | end 25 | 26 | FakeProxyURI = Object.new # :nodoc: 27 | class << FakeProxyURI # :nodoc: 28 | def method_missing(meth, *args) 29 | if %w(scheme host port path query userinfo).member?(meth.to_s) 30 | return nil 31 | end 32 | super 33 | end 34 | end 35 | 36 | # :startdoc: 37 | 38 | ## 39 | # An HTTP Proxy server which proxies GET, HEAD and POST requests. 40 | # 41 | # To create a simple proxy server: 42 | # 43 | # require 'webrick' 44 | # require 'webrick/httpproxy' 45 | # 46 | # proxy = WEBrick::HTTPProxyServer.new Port: 8000 47 | # 48 | # trap 'INT' do proxy.shutdown end 49 | # trap 'TERM' do proxy.shutdown end 50 | # 51 | # proxy.start 52 | # 53 | # See ::new for proxy-specific configuration items. 54 | # 55 | # == Modifying proxied responses 56 | # 57 | # To modify content the proxy server returns use the +:ProxyContentHandler+ 58 | # option: 59 | # 60 | # handler = proc do |req, res| 61 | # if res['content-type'] == 'text/plain' then 62 | # res.body << "\nThis content was proxied!\n" 63 | # end 64 | # end 65 | # 66 | # proxy = 67 | # WEBrick::HTTPProxyServer.new Port: 8000, ProxyContentHandler: handler 68 | 69 | class HTTPProxyServer < HTTPServer 70 | 71 | ## 72 | # Proxy server configurations. The proxy server handles the following 73 | # configuration items in addition to those supported by HTTPServer: 74 | # 75 | # :ProxyAuthProc:: Called with a request and response to authorize a 76 | # request 77 | # :ProxyVia:: Appended to the via header 78 | # :ProxyURI:: The proxy server's URI 79 | # :ProxyContentHandler:: Called with a request and response and allows 80 | # modification of the response 81 | # :ProxyTimeout:: Sets the proxy timeouts to 30 seconds for open and 60 82 | # seconds for read operations 83 | 84 | def initialize(config={}, default=Config::HTTP) 85 | super(config, default) 86 | c = @config 87 | @via = "#{c[:HTTPVersion]} #{c[:ServerName]}:#{c[:Port]}" 88 | end 89 | 90 | # :stopdoc: 91 | def service(req, res) 92 | if req.request_method == "CONNECT" 93 | do_CONNECT(req, res) 94 | elsif req.unparsed_uri =~ %r!^http://! 95 | proxy_service(req, res) 96 | else 97 | super(req, res) 98 | end 99 | end 100 | 101 | def proxy_auth(req, res) 102 | if proc = @config[:ProxyAuthProc] 103 | proc.call(req, res) 104 | end 105 | req.header.delete("proxy-authorization") 106 | end 107 | 108 | def proxy_uri(req, res) 109 | # should return upstream proxy server's URI 110 | return @config[:ProxyURI] 111 | end 112 | 113 | def proxy_service(req, res) 114 | # Proxy Authentication 115 | proxy_auth(req, res) 116 | 117 | begin 118 | public_send("do_#{req.request_method}", req, res) 119 | rescue NoMethodError 120 | raise HTTPStatus::MethodNotAllowed, 121 | "unsupported method `#{req.request_method}'." 122 | rescue => err 123 | logger.debug("#{err.class}: #{err.message}") 124 | raise HTTPStatus::ServiceUnavailable, err.message 125 | end 126 | 127 | # Process contents 128 | if handler = @config[:ProxyContentHandler] 129 | handler.call(req, res) 130 | end 131 | end 132 | 133 | def do_CONNECT(req, res) 134 | # Proxy Authentication 135 | proxy_auth(req, res) 136 | 137 | ua = Thread.current[:WEBrickSocket] # User-Agent 138 | raise HTTPStatus::InternalServerError, 139 | "[BUG] cannot get socket" unless ua 140 | 141 | host, port = req.unparsed_uri.split(":", 2) 142 | # Proxy authentication for upstream proxy server 143 | if proxy = proxy_uri(req, res) 144 | proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0" 145 | if proxy.userinfo 146 | credentials = "Basic " + [proxy.userinfo].pack("m0") 147 | end 148 | host, port = proxy.host, proxy.port 149 | end 150 | 151 | begin 152 | @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.") 153 | os = TCPSocket.new(host, port) # origin server 154 | 155 | if proxy 156 | @logger.debug("CONNECT: sending a Request-Line") 157 | os << proxy_request_line << CRLF 158 | @logger.debug("CONNECT: > #{proxy_request_line}") 159 | if credentials 160 | @logger.debug("CONNECT: sending credentials") 161 | os << "Proxy-Authorization: " << credentials << CRLF 162 | end 163 | os << CRLF 164 | proxy_status_line = os.gets(LF) 165 | @logger.debug("CONNECT: read Status-Line from the upstream server") 166 | @logger.debug("CONNECT: < #{proxy_status_line}") 167 | if %r{^HTTP/\d+\.\d+\s+200\s*} =~ proxy_status_line 168 | while line = os.gets(LF) 169 | break if /\A(#{CRLF}|#{LF})\z/om =~ line 170 | end 171 | else 172 | raise HTTPStatus::BadGateway 173 | end 174 | end 175 | @logger.debug("CONNECT #{host}:#{port}: succeeded") 176 | res.status = HTTPStatus::RC_OK 177 | rescue => ex 178 | @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'") 179 | res.set_error(ex) 180 | raise HTTPStatus::EOFError 181 | ensure 182 | if handler = @config[:ProxyContentHandler] 183 | handler.call(req, res) 184 | end 185 | res.send_response(ua) 186 | access_log(@config, req, res) 187 | 188 | # Should clear request-line not to send the response twice. 189 | # see: HTTPServer#run 190 | req.parse(NullReader) rescue nil 191 | end 192 | 193 | begin 194 | while fds = IO::select([ua, os]) 195 | if fds[0].member?(ua) 196 | buf = ua.readpartial(1024); 197 | @logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent") 198 | os.write(buf) 199 | elsif fds[0].member?(os) 200 | buf = os.readpartial(1024); 201 | @logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}") 202 | ua.write(buf) 203 | end 204 | end 205 | rescue 206 | os.close 207 | @logger.debug("CONNECT #{host}:#{port}: closed") 208 | end 209 | 210 | raise HTTPStatus::EOFError 211 | end 212 | 213 | def do_GET(req, res) 214 | perform_proxy_request(req, res, Net::HTTP::Get) 215 | end 216 | 217 | def do_HEAD(req, res) 218 | perform_proxy_request(req, res, Net::HTTP::Head) 219 | end 220 | 221 | def do_POST(req, res) 222 | perform_proxy_request(req, res, Net::HTTP::Post, req.body_reader) 223 | end 224 | 225 | def do_OPTIONS(req, res) 226 | res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT" 227 | end 228 | 229 | private 230 | 231 | # Some header fields should not be transferred. 232 | HopByHop = %w( connection keep-alive proxy-authenticate upgrade 233 | proxy-authorization te trailers transfer-encoding ) 234 | ShouldNotTransfer = %w( set-cookie proxy-connection ) 235 | def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end 236 | 237 | def choose_header(src, dst) 238 | connections = split_field(src['connection']) 239 | src.each{|key, value| 240 | key = key.downcase 241 | if HopByHop.member?(key) || # RFC2616: 13.5.1 242 | connections.member?(key) || # RFC2616: 14.10 243 | ShouldNotTransfer.member?(key) # pragmatics 244 | @logger.debug("choose_header: `#{key}: #{value}'") 245 | next 246 | end 247 | dst[key] = value 248 | } 249 | end 250 | 251 | # Net::HTTP is stupid about the multiple header fields. 252 | # Here is workaround: 253 | def set_cookie(src, dst) 254 | if str = src['set-cookie'] 255 | cookies = [] 256 | str.split(/,\s*/).each{|token| 257 | if /^[^=]+;/o =~ token 258 | cookies[-1] << ", " << token 259 | elsif /=/o =~ token 260 | cookies << token 261 | else 262 | cookies[-1] << ", " << token 263 | end 264 | } 265 | dst.cookies.replace(cookies) 266 | end 267 | end 268 | 269 | def set_via(h) 270 | if @config[:ProxyVia] 271 | if h['via'] 272 | h['via'] << ", " << @via 273 | else 274 | h['via'] = @via 275 | end 276 | end 277 | end 278 | 279 | def setup_proxy_header(req, res) 280 | # Choose header fields to transfer 281 | header = Hash.new 282 | choose_header(req, header) 283 | set_via(header) 284 | return header 285 | end 286 | 287 | def setup_upstream_proxy_authentication(req, res, header) 288 | if upstream = proxy_uri(req, res) 289 | if upstream.userinfo 290 | header['proxy-authorization'] = 291 | "Basic " + [upstream.userinfo].pack("m0") 292 | end 293 | return upstream 294 | end 295 | return FakeProxyURI 296 | end 297 | 298 | def create_net_http(uri, upstream) 299 | Net::HTTP.new(uri.host, uri.port, upstream.host, upstream.port) 300 | end 301 | 302 | def perform_proxy_request(req, res, req_class, body_stream = nil) 303 | uri = req.request_uri 304 | path = uri.path.dup 305 | path << "?" << uri.query if uri.query 306 | header = setup_proxy_header(req, res) 307 | upstream = setup_upstream_proxy_authentication(req, res, header) 308 | 309 | body_tmp = [] 310 | http = create_net_http(uri, upstream) 311 | req_fib = Fiber.new do 312 | http.start do 313 | if @config[:ProxyTimeout] 314 | ################################## these issues are 315 | http.open_timeout = 30 # secs # necessary (maybe because 316 | http.read_timeout = 60 # secs # Ruby's bug, but why?) 317 | ################################## 318 | end 319 | if body_stream && req['transfer-encoding'] =~ /\bchunked\b/i 320 | header['Transfer-Encoding'] = 'chunked' 321 | end 322 | http_req = req_class.new(path, header) 323 | http_req.body_stream = body_stream if body_stream 324 | http.request(http_req) do |response| 325 | # Persistent connection requirements are mysterious for me. 326 | # So I will close the connection in every response. 327 | res['proxy-connection'] = "close" 328 | res['connection'] = "close" 329 | 330 | # stream Net::HTTP::HTTPResponse to WEBrick::HTTPResponse 331 | res.status = response.code.to_i 332 | res.chunked = response.chunked? 333 | choose_header(response, res) 334 | set_cookie(response, res) 335 | set_via(res) 336 | response.read_body do |buf| 337 | body_tmp << buf 338 | Fiber.yield # wait for res.body Proc#call 339 | end 340 | end # http.request 341 | end 342 | end 343 | req_fib.resume # read HTTP response headers and first chunk of the body 344 | res.body = ->(socket) do 345 | while buf = body_tmp.shift 346 | socket.write(buf) 347 | buf.clear 348 | req_fib.resume # continue response.read_body 349 | end 350 | end 351 | end 352 | # :stopdoc: 353 | end 354 | end 355 | --------------------------------------------------------------------------------