pax_global_header00006660000000000000000000000064137645602430014524gustar00rootroot0000000000000052 comment=650289c70d1bccb3e0cefcf6ede99dc043c5a9dd webrick-1.7.0/000077500000000000000000000000001376456024300131575ustar00rootroot00000000000000webrick-1.7.0/.github/000077500000000000000000000000001376456024300145175ustar00rootroot00000000000000webrick-1.7.0/.github/workflows/000077500000000000000000000000001376456024300165545ustar00rootroot00000000000000webrick-1.7.0/.github/workflows/test.yml000066400000000000000000000010551376456024300202570ustar00rootroot00000000000000name: test on: [push, pull_request] jobs: build: name: build (${{ matrix.ruby }} / ${{ matrix.os }}) strategy: matrix: ruby: [ 2.7, 2.6, 2.5, head ] os: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@master - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Install dependencies run: | gem install bundler --no-document bundle install - name: Run test run: rake test webrick-1.7.0/.gitignore000066400000000000000000000001271376456024300151470ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ webrick-1.7.0/Gemfile000066400000000000000000000000471376456024300144530ustar00rootroot00000000000000source 'https://rubygems.org' gemspec webrick-1.7.0/LICENSE.txt000066400000000000000000000024021376456024300150000ustar00rootroot00000000000000Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. webrick-1.7.0/README.md000066400000000000000000000040661376456024300144440ustar00rootroot00000000000000# Webrick WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server. WEBrick features complete logging of both server operations and HTTP access. WEBrick supports both basic and digest authentication in addition to algorithms not in RFC 2617. 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. WEBrick also includes tools for daemonizing a process and starting a process at a higher privilege level and dropping permissions. ## Installation Add this line to your application's Gemfile: ```ruby gem 'webrick' ``` And then execute: $ bundle Or install it yourself as: $ gem install webrick ## Usage 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: ```ruby require 'webrick' root = File.expand_path '~/public_html' server = WEBrick::HTTPServer.new :Port => 8000, :DocumentRoot => root ``` To run the server you will need to provide a suitable shutdown hook as starting the server blocks the current thread: ```ruby trap 'INT' do server.shutdown end server.start ``` ## Development 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. 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). ## Contributing Bug reports and Patch are welcome on https://bugs.ruby-lang.org/. ## License The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause). webrick-1.7.0/Rakefile000066400000000000000000000003241376456024300146230ustar00rootroot00000000000000require "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new(:test) do |t| t.libs << "test" << "test/lib" t.libs << "lib" t.test_files = FileList['test/**/test_*.rb'] end task :default => :test webrick-1.7.0/bin/000077500000000000000000000000001376456024300137275ustar00rootroot00000000000000webrick-1.7.0/bin/console000077500000000000000000000005261376456024300153220ustar00rootroot00000000000000#!/usr/bin/env ruby require "bundler/setup" require "webrick" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) webrick-1.7.0/bin/setup000077500000000000000000000002031376456024300150100ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here webrick-1.7.0/lib/000077500000000000000000000000001376456024300137255ustar00rootroot00000000000000webrick-1.7.0/lib/webrick.rb000066400000000000000000000155411376456024300157060ustar00rootroot00000000000000# frozen_string_literal: false ## # = WEB server toolkit. # # WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, # a proxy server, and a virtual-host server. WEBrick features complete # logging of both server operations and HTTP access. WEBrick supports both # basic and digest authentication in addition to algorithms not in RFC 2617. # # 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. # # WEBrick also includes tools for daemonizing a process and starting a process # at a higher privilege level and dropping permissions. # # == Security # # *Warning:* WEBrick is not recommended for production. It only implements # basic security checks. # # == Starting an HTTP server # # 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: # # require 'webrick' # # root = File.expand_path '~/public_html' # server = WEBrick::HTTPServer.new :Port => 8000, :DocumentRoot => root # # To run the server you will need to provide a suitable shutdown hook as # starting the server blocks the current thread: # # trap 'INT' do server.shutdown end # # server.start # # == Custom Behavior # # The easiest way to have a server perform custom operations is through # WEBrick::HTTPServer#mount_proc. The block given will be called with a # WEBrick::HTTPRequest with request info and a WEBrick::HTTPResponse which # must be filled in appropriately: # # server.mount_proc '/' do |req, res| # res.body = 'Hello, world!' # end # # Remember that +server.mount_proc+ must precede +server.start+. # # == Servlets # # Advanced custom behavior can be obtained through mounting a subclass of # WEBrick::HTTPServlet::AbstractServlet. Servlets provide more modularity # when writing an HTTP server than mount_proc allows. Here is a simple # servlet: # # class Simple < WEBrick::HTTPServlet::AbstractServlet # def do_GET request, response # status, content_type, body = do_stuff_with request # # response.status = 200 # response['Content-Type'] = 'text/plain' # response.body = 'Hello, World!' # end # end # # To initialize the servlet you mount it on the server: # # server.mount '/simple', Simple # # See WEBrick::HTTPServlet::AbstractServlet for more details. # # == Virtual Hosts # # A server can act as a virtual host for multiple host names. After creating # the listening host, additional hosts that do not listen can be created and # attached as virtual hosts: # # server = WEBrick::HTTPServer.new # ... # # vhost = WEBrick::HTTPServer.new :ServerName => 'vhost.example', # :DoNotListen => true, # ... # vhost.mount '/', ... # # server.virtual_host vhost # # If no +:DocumentRoot+ is provided and no servlets or procs are mounted on the # main server it will return 404 for all URLs. # # == HTTPS # # To create an HTTPS server you only need to enable SSL and provide an SSL # certificate name: # # require 'webrick' # require 'webrick/https' # # cert_name = [ # %w[CN localhost], # ] # # server = WEBrick::HTTPServer.new(:Port => 8000, # :SSLEnable => true, # :SSLCertName => cert_name) # # This will start the server with a self-generated self-signed certificate. # The certificate will be changed every time the server is restarted. # # To create a server with a pre-determined key and certificate you can provide # them: # # require 'webrick' # require 'webrick/https' # require 'openssl' # # cert = OpenSSL::X509::Certificate.new File.read '/path/to/cert.pem' # pkey = OpenSSL::PKey::RSA.new File.read '/path/to/pkey.pem' # # server = WEBrick::HTTPServer.new(:Port => 8000, # :SSLEnable => true, # :SSLCertificate => cert, # :SSLPrivateKey => pkey) # # == Proxy Server # # WEBrick can act as a proxy server: # # require 'webrick' # require 'webrick/httpproxy' # # proxy = WEBrick::HTTPProxyServer.new :Port => 8000 # # trap 'INT' do proxy.shutdown end # # See WEBrick::HTTPProxy for further details including modifying proxied # responses. # # == Basic and Digest authentication # # WEBrick provides both Basic and Digest authentication for regular and proxy # servers. See WEBrick::HTTPAuth, WEBrick::HTTPAuth::BasicAuth and # WEBrick::HTTPAuth::DigestAuth. # # == WEBrick as a daemonized Web Server # # WEBrick can be run as a daemonized server for small loads. # # === Daemonizing # # To start a WEBrick server as a daemon simple run WEBrick::Daemon.start # before starting the server. # # === Dropping Permissions # # WEBrick can be started as one user to gain permission to bind to port 80 or # 443 for serving HTTP or HTTPS traffic then can drop these permissions for # regular operation. To listen on all interfaces for HTTP traffic: # # sockets = WEBrick::Utils.create_listeners nil, 80 # # Then drop privileges: # # WEBrick::Utils.su 'www' # # Then create a server that does not listen by default: # # server = WEBrick::HTTPServer.new :DoNotListen => true, # ... # # Then overwrite the listening sockets with the port 80 sockets: # # server.listeners.replace sockets # # === Logging # # WEBrick can separately log server operations and end-user access. For # server operations: # # log_file = File.open '/var/log/webrick.log', 'a+' # log = WEBrick::Log.new log_file # # For user access logging: # # access_log = [ # [log_file, WEBrick::AccessLog::COMBINED_LOG_FORMAT], # ] # # server = WEBrick::HTTPServer.new :Logger => log, :AccessLog => access_log # # See WEBrick::AccessLog for further log formats. # # === Log Rotation # # To rotate logs in WEBrick on a HUP signal (like syslogd can send), open the # log file in 'a+' mode (as above) and trap 'HUP' to reopen the log file: # # trap 'HUP' do log_file.reopen '/path/to/webrick.log', 'a+' # # == Copyright # # Author: IPR -- Internet Programming with Ruby -- writers # # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. #-- # $IPR: webrick.rb,v 1.12 2002/10/01 17:16:31 gotoyuzo Exp $ module WEBrick end require 'webrick/compat.rb' require 'webrick/version.rb' require 'webrick/config.rb' require 'webrick/log.rb' require 'webrick/server.rb' require_relative 'webrick/utils.rb' require 'webrick/accesslog' require 'webrick/htmlutils.rb' require 'webrick/httputils.rb' require 'webrick/cookie.rb' require 'webrick/httpversion.rb' require 'webrick/httpstatus.rb' require 'webrick/httprequest.rb' require 'webrick/httpresponse.rb' require 'webrick/httpserver.rb' require 'webrick/httpservlet.rb' require 'webrick/httpauth.rb' webrick-1.7.0/lib/webrick/000077500000000000000000000000001376456024300153535ustar00rootroot00000000000000webrick-1.7.0/lib/webrick/accesslog.rb000066400000000000000000000105241376456024300176450ustar00rootroot00000000000000# frozen_string_literal: false #-- # accesslog.rb -- Access log handling utilities # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2002 keita yamaguchi # Copyright (c) 2002 Internet Programming with Ruby writers # # $IPR: accesslog.rb,v 1.1 2002/10/01 17:16:32 gotoyuzo Exp $ module WEBrick ## # AccessLog provides logging to various files in various formats. # # Multiple logs may be written to at the same time: # # access_log = [ # [$stderr, WEBrick::AccessLog::COMMON_LOG_FORMAT], # [$stderr, WEBrick::AccessLog::REFERER_LOG_FORMAT], # ] # # server = WEBrick::HTTPServer.new :AccessLog => access_log # # Custom log formats may be defined. WEBrick::AccessLog provides a subset # of the formatting from Apache's mod_log_config # http://httpd.apache.org/docs/mod/mod_log_config.html#formats. See # AccessLog::setup_params for a list of supported options module AccessLog ## # Raised if a parameter such as %e, %i, %o or %n is used without fetching # a specific field. class AccessLogError < StandardError; end ## # The Common Log Format's time format CLF_TIME_FORMAT = "[%d/%b/%Y:%H:%M:%S %Z]" ## # Common Log Format COMMON_LOG_FORMAT = "%h %l %u %t \"%r\" %s %b" ## # Short alias for Common Log Format CLF = COMMON_LOG_FORMAT ## # Referer Log Format REFERER_LOG_FORMAT = "%{Referer}i -> %U" ## # User-Agent Log Format AGENT_LOG_FORMAT = "%{User-Agent}i" ## # Combined Log Format COMBINED_LOG_FORMAT = "#{CLF} \"%{Referer}i\" \"%{User-agent}i\"" module_function # This format specification is a subset of mod_log_config of Apache: # # %a:: Remote IP address # %b:: Total response size # %e{variable}:: Given variable in ENV # %f:: Response filename # %h:: Remote host name # %{header}i:: Given request header # %l:: Remote logname, always "-" # %m:: Request method # %{attr}n:: Given request attribute from req.attributes # %{header}o:: Given response header # %p:: Server's request port # %{format}p:: The canonical port of the server serving the request or the # actual port or the client's actual port. Valid formats are # canonical, local or remote. # %q:: Request query string # %r:: First line of the request # %s:: Request status # %t:: Time the request was received # %T:: Time taken to process the request # %u:: Remote user from auth # %U:: Unparsed URI # %%:: Literal % def setup_params(config, req, res) params = Hash.new("") params["a"] = req.peeraddr[3] params["b"] = res.sent_size params["e"] = ENV params["f"] = res.filename || "" params["h"] = req.peeraddr[2] params["i"] = req params["l"] = "-" params["m"] = req.request_method params["n"] = req.attributes params["o"] = res params["p"] = req.port params["q"] = req.query_string params["r"] = req.request_line.sub(/\x0d?\x0a\z/o, '') params["s"] = res.status # won't support "%>s" params["t"] = req.request_time params["T"] = Time.now - req.request_time params["u"] = req.user || "-" params["U"] = req.unparsed_uri params["v"] = config[:ServerName] params end ## # Formats +params+ according to +format_string+ which is described in # setup_params. def format(format_string, params) format_string.gsub(/\%(?:\{(.*?)\})?>?([a-zA-Z%])/){ param, spec = $1, $2 case spec[0] when ?e, ?i, ?n, ?o raise AccessLogError, "parameter is required for \"#{spec}\"" unless param (param = params[spec][param]) ? escape(param) : "-" when ?t params[spec].strftime(param || CLF_TIME_FORMAT) when ?p case param when 'remote' escape(params["i"].peeraddr[1].to_s) else escape(params["p"].to_s) end when ?% "%" else escape(params[spec].to_s) end } end ## # Escapes control characters in +data+ def escape(data) data = data.gsub(/[[:cntrl:]\\]+/) {$&.dump[1...-1]} data.untaint if RUBY_VERSION < '2.7' data end end end webrick-1.7.0/lib/webrick/cgi.rb000066400000000000000000000200261376456024300164420ustar00rootroot00000000000000# frozen_string_literal: false # # cgi.rb -- Yet another CGI library # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $Id$ require_relative "httprequest" require_relative "httpresponse" require_relative "config" require "stringio" module WEBrick # A CGI library using WEBrick requests and responses. # # Example: # # class MyCGI < WEBrick::CGI # def do_GET req, res # res.body = 'it worked!' # res.status = 200 # end # end # # MyCGI.new.start class CGI # The CGI error exception class CGIError = Class.new(StandardError) ## # The CGI configuration. This is based on WEBrick::Config::HTTP attr_reader :config ## # The CGI logger attr_reader :logger ## # Creates a new CGI interface. # # The first argument in +args+ is a configuration hash which would update # WEBrick::Config::HTTP. # # Any remaining arguments are stored in the @options instance # variable for use by a subclass. def initialize(*args) if defined?(MOD_RUBY) unless ENV.has_key?("GATEWAY_INTERFACE") Apache.request.setup_cgi_env end end if %r{HTTP/(\d+\.\d+)} =~ ENV["SERVER_PROTOCOL"] httpv = $1 end @config = WEBrick::Config::HTTP.dup.update( :ServerSoftware => ENV["SERVER_SOFTWARE"] || "null", :HTTPVersion => HTTPVersion.new(httpv || "1.0"), :RunOnCGI => true, # to detect if it runs on CGI. :NPH => false # set true to run as NPH script. ) if config = args.shift @config.update(config) end @config[:Logger] ||= WEBrick::BasicLog.new($stderr) @logger = @config[:Logger] @options = args end ## # Reads +key+ from the configuration def [](key) @config[key] end ## # Starts the CGI process with the given environment +env+ and standard # input and output +stdin+ and +stdout+. def start(env=ENV, stdin=$stdin, stdout=$stdout) sock = WEBrick::CGI::Socket.new(@config, env, stdin, stdout) req = HTTPRequest.new(@config) res = HTTPResponse.new(@config) unless @config[:NPH] or defined?(MOD_RUBY) def res.setup_header unless @header["status"] phrase = HTTPStatus::reason_phrase(@status) @header["status"] = "#{@status} #{phrase}" end super end def res.status_line "" end end begin req.parse(sock) req.script_name = (env["SCRIPT_NAME"] || File.expand_path($0)).dup req.path_info = (env["PATH_INFO"] || "").dup req.query_string = env["QUERY_STRING"] req.user = env["REMOTE_USER"] res.request_method = req.request_method res.request_uri = req.request_uri res.request_http_version = req.http_version res.keep_alive = req.keep_alive? self.service(req, res) rescue HTTPStatus::Error => ex res.set_error(ex) rescue HTTPStatus::Status => ex res.status = ex.code rescue Exception => ex @logger.error(ex) res.set_error(ex, true) ensure req.fixup if defined?(MOD_RUBY) res.setup_header Apache.request.status_line = "#{res.status} #{res.reason_phrase}" Apache.request.status = res.status table = Apache.request.headers_out res.header.each{|key, val| case key when /^content-encoding$/i Apache::request.content_encoding = val when /^content-type$/i Apache::request.content_type = val else table[key] = val.to_s end } res.cookies.each{|cookie| table.add("Set-Cookie", cookie.to_s) } Apache.request.send_http_header res.send_body(sock) else res.send_response(sock) end end end ## # Services the request +req+ which will fill in the response +res+. See # WEBrick::HTTPServlet::AbstractServlet#service for details. def service(req, res) method_name = "do_" + req.request_method.gsub(/-/, "_") if respond_to?(method_name) __send__(method_name, req, res) else raise HTTPStatus::MethodNotAllowed, "unsupported method `#{req.request_method}'." end end ## # Provides HTTP socket emulation from the CGI environment class Socket # :nodoc: include Enumerable private def initialize(config, env, stdin, stdout) @config = config @env = env @header_part = StringIO.new @body_part = stdin @out_port = stdout @out_port.binmode @server_addr = @env["SERVER_ADDR"] || "0.0.0.0" @server_name = @env["SERVER_NAME"] @server_port = @env["SERVER_PORT"] @remote_addr = @env["REMOTE_ADDR"] @remote_host = @env["REMOTE_HOST"] || @remote_addr @remote_port = @env["REMOTE_PORT"] || 0 begin @header_part << request_line << CRLF setup_header @header_part << CRLF @header_part.rewind rescue Exception raise CGIError, "invalid CGI environment" end end def request_line meth = @env["REQUEST_METHOD"] || "GET" unless url = @env["REQUEST_URI"] url = (@env["SCRIPT_NAME"] || File.expand_path($0)).dup url << @env["PATH_INFO"].to_s url = WEBrick::HTTPUtils.escape_path(url) if query_string = @env["QUERY_STRING"] unless query_string.empty? url << "?" << query_string end end end # we cannot get real HTTP version of client ;) httpv = @config[:HTTPVersion] return "#{meth} #{url} HTTP/#{httpv}" end def setup_header @env.each{|key, value| case key when "CONTENT_TYPE", "CONTENT_LENGTH" add_header(key.gsub(/_/, "-"), value) when /^HTTP_(.*)/ add_header($1.gsub(/_/, "-"), value) end } end def add_header(hdrname, value) unless value.empty? @header_part << hdrname << ": " << value << CRLF end end def input @header_part.eof? ? @body_part : @header_part end public def peeraddr [nil, @remote_port, @remote_host, @remote_addr] end def addr [nil, @server_port, @server_name, @server_addr] end def gets(eol=LF, size=nil) input.gets(eol, size) end def read(size=nil) input.read(size) end def each input.each{|line| yield(line) } end def eof? input.eof? end def <<(data) @out_port << data end def write(data) @out_port.write(data) end def cert return nil unless defined?(OpenSSL) if pem = @env["SSL_SERVER_CERT"] OpenSSL::X509::Certificate.new(pem) unless pem.empty? end end def peer_cert return nil unless defined?(OpenSSL) if pem = @env["SSL_CLIENT_CERT"] OpenSSL::X509::Certificate.new(pem) unless pem.empty? end end def peer_cert_chain return nil unless defined?(OpenSSL) if @env["SSL_CLIENT_CERT_CHAIN_0"] keys = @env.keys certs = keys.sort.collect{|k| if /^SSL_CLIENT_CERT_CHAIN_\d+$/ =~ k if pem = @env[k] OpenSSL::X509::Certificate.new(pem) unless pem.empty? end end } certs.compact end end def cipher return nil unless defined?(OpenSSL) if cipher = @env["SSL_CIPHER"] ret = [ cipher ] ret << @env["SSL_PROTOCOL"] ret << @env["SSL_CIPHER_USEKEYSIZE"] ret << @env["SSL_CIPHER_ALGKEYSIZE"] ret end end end end end webrick-1.7.0/lib/webrick/compat.rb000066400000000000000000000016571376456024300171740ustar00rootroot00000000000000# frozen_string_literal: false # # compat.rb -- cross platform compatibility # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2002 GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: compat.rb,v 1.6 2002/10/01 17:16:32 gotoyuzo Exp $ ## # System call error module used by webrick for cross platform compatibility. # # EPROTO:: protocol error # ECONNRESET:: remote host reset the connection request # ECONNABORTED:: Client sent TCP reset (RST) before server has accepted the # connection requested by client. # module Errno ## # Protocol error. class EPROTO < SystemCallError; end ## # Remote host reset the connection request. class ECONNRESET < SystemCallError; end ## # Client sent TCP reset (RST) before server has accepted the connection # requested by client. class ECONNABORTED < SystemCallError; end end webrick-1.7.0/lib/webrick/config.rb000066400000000000000000000133121376456024300171450ustar00rootroot00000000000000# frozen_string_literal: false # # config.rb -- Default configurations. # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: config.rb,v 1.52 2003/07/22 19:20:42 gotoyuzo Exp $ require_relative 'version' require_relative 'httpversion' require_relative 'httputils' require_relative 'utils' require_relative 'log' module WEBrick module Config LIBDIR = File::dirname(__FILE__) # :nodoc: # for GenericServer General = Hash.new { |hash, key| case key when :ServerName hash[key] = Utils.getservername else nil end }.update( :BindAddress => nil, # "0.0.0.0" or "::" or nil :Port => nil, # users MUST specify this!! :MaxClients => 100, # maximum number of the concurrent connections :ServerType => nil, # default: WEBrick::SimpleServer :Logger => nil, # default: WEBrick::Log.new :ServerSoftware => "WEBrick/#{WEBrick::VERSION} " + "(Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})", :TempDir => ENV['TMPDIR']||ENV['TMP']||ENV['TEMP']||'/tmp', :DoNotListen => false, :StartCallback => nil, :StopCallback => nil, :AcceptCallback => nil, :DoNotReverseLookup => true, :ShutdownSocketWithoutClose => false, ) # for HTTPServer, HTTPRequest, HTTPResponse ... HTTP = General.dup.update( :Port => 80, :RequestTimeout => 30, :HTTPVersion => HTTPVersion.new("1.1"), :AccessLog => nil, :MimeTypes => HTTPUtils::DefaultMimeTypes, :DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"], :DocumentRoot => nil, :DocumentRootOptions => { :FancyIndexing => true }, :RequestCallback => nil, :ServerAlias => nil, :InputBufferSize => 65536, # input buffer size in reading request body :OutputBufferSize => 65536, # output buffer size in sending File or IO # for HTTPProxyServer :ProxyAuthProc => nil, :ProxyContentHandler => nil, :ProxyVia => true, :ProxyTimeout => true, :ProxyURI => nil, :CGIInterpreter => nil, :CGIPathEnv => nil, # workaround: if Request-URIs contain 8bit chars, # they should be escaped before calling of URI::parse(). :Escape8bitURI => false ) ## # Default configuration for WEBrick::HTTPServlet::FileHandler # # :AcceptableLanguages:: # Array of languages allowed for accept-language. There is no default # :DirectoryCallback:: # Allows preprocessing of directory requests. There is no default # callback. # :FancyIndexing:: # If true, show an index for directories. The default is true. # :FileCallback:: # Allows preprocessing of file requests. There is no default callback. # :HandlerCallback:: # Allows preprocessing of requests. There is no default callback. # :HandlerTable:: # Maps file suffixes to file handlers. DefaultFileHandler is used by # default but any servlet can be used. # :NondisclosureName:: # Do not show files matching this array of globs. .ht* and *~ are # excluded by default. # :UserDir:: # Directory inside ~user to serve content from for /~user requests. # Only works if mounted on /. Disabled by default. FileHandler = { :NondisclosureName => [".ht*", "*~"], :FancyIndexing => false, :HandlerTable => {}, :HandlerCallback => nil, :DirectoryCallback => nil, :FileCallback => nil, :UserDir => nil, # e.g. "public_html" :AcceptableLanguages => [] # ["en", "ja", ... ] } ## # Default configuration for WEBrick::HTTPAuth::BasicAuth # # :AutoReloadUserDB:: Reload the user database provided by :UserDB # automatically? BasicAuth = { :AutoReloadUserDB => true, } ## # Default configuration for WEBrick::HTTPAuth::DigestAuth. # # :Algorithm:: MD5, MD5-sess (default), SHA1, SHA1-sess # :Domain:: An Array of URIs that define the protected space # :Qop:: 'auth' for authentication, 'auth-int' for integrity protection or # both # :UseOpaque:: Should the server send opaque values to the client? This # helps prevent replay attacks. # :CheckNc:: Should the server check the nonce count? This helps the # server detect replay attacks. # :UseAuthenticationInfoHeader:: Should the server send an # AuthenticationInfo header? # :AutoReloadUserDB:: Reload the user database provided by :UserDB # automatically? # :NonceExpirePeriod:: How long should we store used nonces? Default is # 30 minutes. # :NonceExpireDelta:: How long is a nonce valid? Default is 1 minute # :InternetExplorerHack:: Hack which allows Internet Explorer to work. # :OperaHack:: Hack which allows Opera to work. DigestAuth = { :Algorithm => 'MD5-sess', # or 'MD5' :Domain => nil, # an array includes domain names. :Qop => [ 'auth' ], # 'auth' or 'auth-int' or both. :UseOpaque => true, :UseNextNonce => false, :CheckNc => false, :UseAuthenticationInfoHeader => true, :AutoReloadUserDB => true, :NonceExpirePeriod => 30*60, :NonceExpireDelta => 60, :InternetExplorerHack => true, :OperaHack => true, } end end webrick-1.7.0/lib/webrick/cookie.rb000066400000000000000000000076461376456024300171660ustar00rootroot00000000000000# frozen_string_literal: false # # cookie.rb -- Cookie class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: cookie.rb,v 1.16 2002/09/21 12:23:35 gotoyuzo Exp $ require 'time' require_relative 'httputils' module WEBrick ## # Processes HTTP cookies class Cookie ## # The cookie name attr_reader :name ## # The cookie value attr_accessor :value ## # The cookie version attr_accessor :version ## # The cookie domain attr_accessor :domain ## # The cookie path attr_accessor :path ## # Is this a secure cookie? attr_accessor :secure ## # The cookie comment attr_accessor :comment ## # The maximum age of the cookie attr_accessor :max_age #attr_accessor :comment_url, :discard, :port ## # Creates a new cookie with the given +name+ and +value+ def initialize(name, value) @name = name @value = value @version = 0 # Netscape Cookie @domain = @path = @secure = @comment = @max_age = @expires = @comment_url = @discard = @port = nil end ## # Sets the cookie expiration to the time +t+. The expiration time may be # a false value to disable expiration or a Time or HTTP format time string # to set the expiration date. def expires=(t) @expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s) end ## # Retrieves the expiration time as a Time def expires @expires && Time.parse(@expires) end ## # The cookie string suitable for use in an HTTP header def to_s ret = "" ret << @name << "=" << @value ret << "; " << "Version=" << @version.to_s if @version > 0 ret << "; " << "Domain=" << @domain if @domain ret << "; " << "Expires=" << @expires if @expires ret << "; " << "Max-Age=" << @max_age.to_s if @max_age ret << "; " << "Comment=" << @comment if @comment ret << "; " << "Path=" << @path if @path ret << "; " << "Secure" if @secure ret end ## # Parses a Cookie field sent from the user-agent. Returns an array of # cookies. def self.parse(str) if str ret = [] cookie = nil ver = 0 str.split(/;\s+/).each{|x| key, val = x.split(/=/,2) val = val ? HTTPUtils::dequote(val) : "" case key when "$Version"; ver = val.to_i when "$Path"; cookie.path = val when "$Domain"; cookie.domain = val when "$Port"; cookie.port = val else ret << cookie if cookie cookie = self.new(key, val) cookie.version = ver end } ret << cookie if cookie ret end end ## # Parses the cookie in +str+ def self.parse_set_cookie(str) cookie_elem = str.split(/;/) first_elem = cookie_elem.shift first_elem.strip! key, value = first_elem.split(/=/, 2) cookie = new(key, HTTPUtils.dequote(value)) cookie_elem.each{|pair| pair.strip! key, value = pair.split(/=/, 2) if value value = HTTPUtils.dequote(value.strip) end case key.downcase when "domain" then cookie.domain = value when "path" then cookie.path = value when "expires" then cookie.expires = value when "max-age" then cookie.max_age = Integer(value) when "comment" then cookie.comment = value when "version" then cookie.version = Integer(value) when "secure" then cookie.secure = true end } return cookie end ## # Parses the cookies in +str+ def self.parse_set_cookies(str) return str.split(/,(?=[^;,]*=)|,$/).collect{|c| parse_set_cookie(c) } end end end webrick-1.7.0/lib/webrick/htmlutils.rb000066400000000000000000000013071376456024300177260ustar00rootroot00000000000000# frozen_string_literal: false #-- # htmlutils.rb -- HTMLUtils Module # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: htmlutils.rb,v 1.7 2002/09/21 12:23:35 gotoyuzo Exp $ module WEBrick module HTMLUtils ## # Escapes &, ", > and < in +string+ def escape(string) return "" unless string str = string.b str.gsub!(/&/n, '&') str.gsub!(/\"/n, '"') str.gsub!(/>/n, '>') str.gsub!(/ 'DigestAuth example realm' } # # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' # htpasswd.auth_type = WEBrick::HTTPAuth::DigestAuth # htpasswd.set_passwd config[:Realm], 'username', 'password' # htpasswd.flush # # The +:Realm+ is used to provide different access to different groups # across several resources on a server. Typically you'll need only one # realm for a server. # # This database can be used to create an authenticator: # # config[:UserDB] = htpasswd # # digest_auth = WEBrick::HTTPAuth::DigestAuth.new config # # To authenticate a request call #authenticate with a request and response # object in a servlet: # # def do_GET req, res # @authenticator.authenticate req, res # end # # For digest authentication the authenticator must not be created every # request, it must be passed in as an option via WEBrick::HTTPServer#mount. module HTTPAuth module_function def _basic_auth(req, res, realm, req_field, res_field, err_type, block) # :nodoc: user = pass = nil if /^Basic\s+(.*)/o =~ req[req_field] userpass = $1 user, pass = userpass.unpack("m*")[0].split(":", 2) end if block.call(user, pass) req.user = user return end res[res_field] = "Basic realm=\"#{realm}\"" raise err_type end ## # Simple wrapper for providing basic authentication for a request. When # called with a request +req+, response +res+, authentication +realm+ and # +block+ the block will be called with a +username+ and +password+. If # the block returns true the request is allowed to continue, otherwise an # HTTPStatus::Unauthorized error is raised. def basic_auth(req, res, realm, &block) # :yield: username, password _basic_auth(req, res, realm, "Authorization", "WWW-Authenticate", HTTPStatus::Unauthorized, block) end ## # Simple wrapper for providing basic authentication for a proxied request. # When called with a request +req+, response +res+, authentication +realm+ # and +block+ the block will be called with a +username+ and +password+. # If the block returns true the request is allowed to continue, otherwise # an HTTPStatus::ProxyAuthenticationRequired error is raised. def proxy_basic_auth(req, res, realm, &block) # :yield: username, password _basic_auth(req, res, realm, "Proxy-Authorization", "Proxy-Authenticate", HTTPStatus::ProxyAuthenticationRequired, block) end end end webrick-1.7.0/lib/webrick/httpauth/000077500000000000000000000000001376456024300172145ustar00rootroot00000000000000webrick-1.7.0/lib/webrick/httpauth/authenticator.rb000066400000000000000000000060411376456024300224140ustar00rootroot00000000000000# frozen_string_literal: false #-- # httpauth/authenticator.rb -- Authenticator mix-in module. # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: authenticator.rb,v 1.3 2003/02/20 07:15:47 gotoyuzo Exp $ module WEBrick module HTTPAuth ## # Module providing generic support for both Digest and Basic # authentication schemes. module Authenticator RequestField = "Authorization" # :nodoc: ResponseField = "WWW-Authenticate" # :nodoc: ResponseInfoField = "Authentication-Info" # :nodoc: AuthException = HTTPStatus::Unauthorized # :nodoc: ## # Method of authentication, must be overridden by the including class AuthScheme = nil ## # The realm this authenticator covers attr_reader :realm ## # The user database for this authenticator attr_reader :userdb ## # The logger for this authenticator attr_reader :logger private # :stopdoc: ## # Initializes the authenticator from +config+ def check_init(config) [:UserDB, :Realm].each{|sym| unless config[sym] raise ArgumentError, "Argument #{sym.inspect} missing." end } @realm = config[:Realm] @userdb = config[:UserDB] @logger = config[:Logger] || Log::new($stderr) @reload_db = config[:AutoReloadUserDB] @request_field = self::class::RequestField @response_field = self::class::ResponseField @resp_info_field = self::class::ResponseInfoField @auth_exception = self::class::AuthException @auth_scheme = self::class::AuthScheme end ## # Ensures +req+ has credentials that can be authenticated. def check_scheme(req) unless credentials = req[@request_field] error("no credentials in the request.") return nil end unless match = /^#{@auth_scheme}\s+/i.match(credentials) error("invalid scheme in %s.", credentials) info("%s: %s", @request_field, credentials) if $DEBUG return nil end return match.post_match end def log(meth, fmt, *args) msg = format("%s %s: ", @auth_scheme, @realm) msg << fmt % args @logger.__send__(meth, msg) end def error(fmt, *args) if @logger.error? log(:error, fmt, *args) end end def info(fmt, *args) if @logger.info? log(:info, fmt, *args) end end # :startdoc: end ## # Module providing generic support for both Digest and Basic # authentication schemes for proxies. module ProxyAuthenticator RequestField = "Proxy-Authorization" # :nodoc: ResponseField = "Proxy-Authenticate" # :nodoc: InfoField = "Proxy-Authentication-Info" # :nodoc: AuthException = HTTPStatus::ProxyAuthenticationRequired # :nodoc: end end end webrick-1.7.0/lib/webrick/httpauth/basicauth.rb000066400000000000000000000064001376456024300215040ustar00rootroot00000000000000# frozen_string_literal: false # # httpauth/basicauth.rb -- HTTP basic access authentication # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: basicauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ require_relative '../config' require_relative '../httpstatus' require_relative 'authenticator' module WEBrick module HTTPAuth ## # Basic Authentication for WEBrick # # Use this class to add basic authentication to a WEBrick servlet. # # Here is an example of how to set up a BasicAuth: # # config = { :Realm => 'BasicAuth example realm' } # # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file', password_hash: :bcrypt # htpasswd.set_passwd config[:Realm], 'username', 'password' # htpasswd.flush # # config[:UserDB] = htpasswd # # basic_auth = WEBrick::HTTPAuth::BasicAuth.new config class BasicAuth include Authenticator AuthScheme = "Basic" # :nodoc: ## # Used by UserDB to create a basic password entry def self.make_passwd(realm, user, pass) pass ||= "" pass.crypt(Utils::random_string(2)) end attr_reader :realm, :userdb, :logger ## # Creates a new BasicAuth instance. # # See WEBrick::Config::BasicAuth for default configuration entries # # You must supply the following configuration entries: # # :Realm:: The name of the realm being protected. # :UserDB:: A database of usernames and passwords. # A WEBrick::HTTPAuth::Htpasswd instance should be used. def initialize(config, default=Config::BasicAuth) check_init(config) @config = default.dup.update(config) end ## # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if # the authentication was not correct. def authenticate(req, res) unless basic_credentials = check_scheme(req) challenge(req, res) end userid, password = basic_credentials.unpack("m*")[0].split(":", 2) password ||= "" if userid.empty? error("user id was not given.") challenge(req, res) end unless encpass = @userdb.get_passwd(@realm, userid, @reload_db) error("%s: the user is not allowed.", userid) challenge(req, res) end case encpass when /\A\$2[aby]\$/ password_matches = BCrypt::Password.new(encpass.sub(/\A\$2[aby]\$/, '$2a$')) == password else password_matches = password.crypt(encpass) == encpass end unless password_matches error("%s: password unmatch.", userid) challenge(req, res) end info("%s: authentication succeeded.", userid) req.user = userid end ## # Returns a challenge response which asks for authentication information def challenge(req, res) res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\"" raise @auth_exception end end ## # Basic authentication for proxy servers. See BasicAuth for details. class ProxyBasicAuth < BasicAuth include ProxyAuthenticator end end end webrick-1.7.0/lib/webrick/httpauth/digestauth.rb000066400000000000000000000314571376456024300217140ustar00rootroot00000000000000# frozen_string_literal: false # # httpauth/digestauth.rb -- HTTP digest access authentication # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. # Copyright (c) 2003 H.M. # # The original implementation is provided by H.M. # URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name= # %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB # # $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $ require_relative '../config' require_relative '../httpstatus' require_relative 'authenticator' require 'digest/md5' require 'digest/sha1' module WEBrick module HTTPAuth ## # RFC 2617 Digest Access Authentication for WEBrick # # Use this class to add digest authentication to a WEBrick servlet. # # Here is an example of how to set up DigestAuth: # # config = { :Realm => 'DigestAuth example realm' } # # htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' # htdigest.set_passwd config[:Realm], 'username', 'password' # htdigest.flush # # config[:UserDB] = htdigest # # digest_auth = WEBrick::HTTPAuth::DigestAuth.new config # # When using this as with a servlet be sure not to create a new DigestAuth # object in the servlet's #initialize. By default WEBrick creates a new # servlet instance for every request and the DigestAuth object must be # used across requests. class DigestAuth include Authenticator AuthScheme = "Digest" # :nodoc: ## # Struct containing the opaque portion of the digest authentication OpaqueInfo = Struct.new(:time, :nonce, :nc) # :nodoc: ## # Digest authentication algorithm attr_reader :algorithm ## # Quality of protection. RFC 2617 defines "auth" and "auth-int" attr_reader :qop ## # Used by UserDB to create a digest password entry def self.make_passwd(realm, user, pass) pass ||= "" Digest::MD5::hexdigest([user, realm, pass].join(":")) end ## # Creates a new DigestAuth instance. Be sure to use the same DigestAuth # instance for multiple requests as it saves state between requests in # order to perform authentication. # # See WEBrick::Config::DigestAuth for default configuration entries # # You must supply the following configuration entries: # # :Realm:: The name of the realm being protected. # :UserDB:: A database of usernames and passwords. # A WEBrick::HTTPAuth::Htdigest instance should be used. def initialize(config, default=Config::DigestAuth) check_init(config) @config = default.dup.update(config) @algorithm = @config[:Algorithm] @domain = @config[:Domain] @qop = @config[:Qop] @use_opaque = @config[:UseOpaque] @use_next_nonce = @config[:UseNextNonce] @check_nc = @config[:CheckNc] @use_auth_info_header = @config[:UseAuthenticationInfoHeader] @nonce_expire_period = @config[:NonceExpirePeriod] @nonce_expire_delta = @config[:NonceExpireDelta] @internet_explorer_hack = @config[:InternetExplorerHack] case @algorithm when 'MD5','MD5-sess' @h = Digest::MD5 when 'SHA1','SHA1-sess' # it is a bonus feature :-) @h = Digest::SHA1 else msg = format('Algorithm "%s" is not supported.', @algorithm) raise ArgumentError.new(msg) end @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid) @opaques = {} @last_nonce_expire = Time.now @mutex = Thread::Mutex.new end ## # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if # the authentication was not correct. def authenticate(req, res) unless result = @mutex.synchronize{ _authenticate(req, res) } challenge(req, res) end if result == :nonce_is_stale challenge(req, res, true) end return true end ## # Returns a challenge response which asks for authentication information def challenge(req, res, stale=false) nonce = generate_next_nonce(req) if @use_opaque opaque = generate_opaque(req) @opaques[opaque].nonce = nonce end param = Hash.new param["realm"] = HTTPUtils::quote(@realm) param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain param["nonce"] = HTTPUtils::quote(nonce) param["opaque"] = HTTPUtils::quote(opaque) if opaque param["stale"] = stale.to_s param["algorithm"] = @algorithm param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop res[@response_field] = "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ") info("%s: %s", @response_field, res[@response_field]) if $DEBUG raise @auth_exception end private # :stopdoc: MustParams = ['username','realm','nonce','uri','response'] MustParamsAuth = ['cnonce','nc'] def _authenticate(req, res) unless digest_credentials = check_scheme(req) return false end auth_req = split_param_value(digest_credentials) if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" req_params = MustParams + MustParamsAuth else req_params = MustParams end req_params.each{|key| unless auth_req.has_key?(key) error('%s: parameter missing. "%s"', auth_req['username'], key) raise HTTPStatus::BadRequest end } if !check_uri(req, auth_req) raise HTTPStatus::BadRequest end if auth_req['realm'] != @realm error('%s: realm unmatch. "%s" for "%s"', auth_req['username'], auth_req['realm'], @realm) return false end auth_req['algorithm'] ||= 'MD5' if auth_req['algorithm'].upcase != @algorithm.upcase error('%s: algorithm unmatch. "%s" for "%s"', auth_req['username'], auth_req['algorithm'], @algorithm) return false end if (@qop.nil? && auth_req.has_key?('qop')) || (@qop && (! @qop.member?(auth_req['qop']))) error('%s: the qop is not allowed. "%s"', auth_req['username'], auth_req['qop']) return false end password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db) unless password error('%s: the user is not allowed.', auth_req['username']) return false end nonce_is_invalid = false if @use_opaque info("@opaque = %s", @opaque.inspect) if $DEBUG if !(opaque = auth_req['opaque']) error('%s: opaque is not given.', auth_req['username']) nonce_is_invalid = true elsif !(opaque_struct = @opaques[opaque]) error('%s: invalid opaque is given.', auth_req['username']) nonce_is_invalid = true elsif !check_opaque(opaque_struct, req, auth_req) @opaques.delete(auth_req['opaque']) nonce_is_invalid = true end elsif !check_nonce(req, auth_req) nonce_is_invalid = true end if /-sess$/i =~ auth_req['algorithm'] ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce']) else ha1 = password end if auth_req['qop'] == "auth" || auth_req['qop'] == nil ha2 = hexdigest(req.request_method, auth_req['uri']) ha2_res = hexdigest("", auth_req['uri']) elsif auth_req['qop'] == "auth-int" body_digest = @h.new req.body { |chunk| body_digest.update(chunk) } body_digest = body_digest.hexdigest ha2 = hexdigest(req.request_method, auth_req['uri'], body_digest) ha2_res = hexdigest("", auth_req['uri'], body_digest) end if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key| auth_req[key] }.join(':') digest = hexdigest(ha1, param2, ha2) digest_res = hexdigest(ha1, param2, ha2_res) else digest = hexdigest(ha1, auth_req['nonce'], ha2) digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res) end if digest != auth_req['response'] error("%s: digest unmatch.", auth_req['username']) return false elsif nonce_is_invalid error('%s: digest is valid, but nonce is not valid.', auth_req['username']) return :nonce_is_stale elsif @use_auth_info_header auth_info = { 'nextnonce' => generate_next_nonce(req), 'rspauth' => digest_res } if @use_opaque opaque_struct.time = req.request_time opaque_struct.nonce = auth_info['nextnonce'] opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1) end if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int" ['qop','cnonce','nc'].each{|key| auth_info[key] = auth_req[key] } end res[@resp_info_field] = auth_info.keys.map{|key| if key == 'nc' key + '=' + auth_info[key] else key + "=" + HTTPUtils::quote(auth_info[key]) end }.join(', ') end info('%s: authentication succeeded.', auth_req['username']) req.user = auth_req['username'] return true end def split_param_value(string) ret = {} string.scan(/\G\s*([\w\-.*%!]+)=\s*(?:\"((?>\\.|[^\"])*)\"|([^,\"]*))\s*,?/) do ret[$1] = $3 || $2.gsub(/\\(.)/, "\\1") end ret end def generate_next_nonce(req) now = "%012d" % req.request_time.to_i pk = hexdigest(now, @instance_key)[0,32] nonce = [now + ":" + pk].pack("m0") # it has 60 length of chars. nonce end def check_nonce(req, auth_req) username = auth_req['username'] nonce = auth_req['nonce'] pub_time, pk = nonce.unpack("m*")[0].split(":", 2) if (!pub_time || !pk) error("%s: empty nonce is given", username) return false elsif (hexdigest(pub_time, @instance_key)[0,32] != pk) error("%s: invalid private-key: %s for %s", username, hexdigest(pub_time, @instance_key)[0,32], pk) return false end diff_time = req.request_time.to_i - pub_time.to_i if (diff_time < 0) error("%s: difference of time-stamp is negative.", username) return false elsif diff_time > @nonce_expire_period error("%s: nonce is expired.", username) return false end return true end def generate_opaque(req) @mutex.synchronize{ now = req.request_time if now - @last_nonce_expire > @nonce_expire_delta @opaques.delete_if{|key,val| (now - val.time) > @nonce_expire_period } @last_nonce_expire = now end begin opaque = Utils::random_string(16) end while @opaques[opaque] @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001') opaque } end def check_opaque(opaque_struct, req, auth_req) if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce) error('%s: nonce unmatched. "%s" for "%s"', auth_req['username'], auth_req['nonce'], opaque_struct.nonce) return false elsif !check_nonce(req, auth_req) return false end if (@check_nc && auth_req['nc'] != opaque_struct.nc) error('%s: nc unmatched."%s" for "%s"', auth_req['username'], auth_req['nc'], opaque_struct.nc) return false end true end def check_uri(req, auth_req) uri = auth_req['uri'] if uri != req.request_uri.to_s && uri != req.unparsed_uri && (@internet_explorer_hack && uri != req.path) error('%s: uri unmatch. "%s" for "%s"', auth_req['username'], auth_req['uri'], req.request_uri.to_s) return false end true end def hexdigest(*args) @h.hexdigest(args.join(":")) end # :startdoc: end ## # Digest authentication for proxy servers. See DigestAuth for details. class ProxyDigestAuth < DigestAuth include ProxyAuthenticator private def check_uri(req, auth_req) # :nodoc: return true end end end end webrick-1.7.0/lib/webrick/httpauth/htdigest.rb000066400000000000000000000066731376456024300213700ustar00rootroot00000000000000# frozen_string_literal: false # # httpauth/htdigest.rb -- Apache compatible htdigest file # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: htdigest.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ require_relative 'userdb' require_relative 'digestauth' require 'tempfile' module WEBrick module HTTPAuth ## # Htdigest accesses apache-compatible digest password files. Passwords are # matched to a realm where they are valid. For security, the path for a # digest password database should be stored outside of the paths available # to the HTTP server. # # Htdigest is intended for use with WEBrick::HTTPAuth::DigestAuth and # stores passwords using cryptographic hashes. # # htpasswd = WEBrick::HTTPAuth::Htdigest.new 'my_password_file' # htpasswd.set_passwd 'my realm', 'username', 'password' # htpasswd.flush class Htdigest include UserDB ## # Open a digest password database at +path+ def initialize(path) @path = path @mtime = Time.at(0) @digest = Hash.new @mutex = Thread::Mutex::new @auth_type = DigestAuth File.open(@path,"a").close unless File.exist?(@path) reload end ## # Reloads passwords from the database def reload mtime = File::mtime(@path) if mtime > @mtime @digest.clear File.open(@path){|io| while line = io.gets line.chomp! user, realm, pass = line.split(/:/, 3) unless @digest[realm] @digest[realm] = Hash.new end @digest[realm][user] = pass end } @mtime = mtime end end ## # Flush the password database. If +output+ is given the database will # be written there instead of to the original path. def flush(output=nil) output ||= @path tmp = Tempfile.create("htpasswd", File::dirname(output)) renamed = false begin each{|item| tmp.puts(item.join(":")) } tmp.close File::rename(tmp.path, output) renamed = true ensure tmp.close File.unlink(tmp.path) if !renamed end end ## # Retrieves a password from the database for +user+ in +realm+. If # +reload_db+ is true the database will be reloaded first. def get_passwd(realm, user, reload_db) reload() if reload_db if hash = @digest[realm] hash[user] end end ## # Sets a password in the database for +user+ in +realm+ to +pass+. def set_passwd(realm, user, pass) @mutex.synchronize{ unless @digest[realm] @digest[realm] = Hash.new end @digest[realm][user] = make_passwd(realm, user, pass) } end ## # Removes a password from the database for +user+ in +realm+. def delete_passwd(realm, user) if hash = @digest[realm] hash.delete(user) end end ## # Iterate passwords in the database. def each # :yields: [user, realm, password_hash] @digest.keys.sort.each{|realm| hash = @digest[realm] hash.keys.sort.each{|user| yield([user, realm, hash[user]]) } } end end end end webrick-1.7.0/lib/webrick/httpauth/htgroup.rb000066400000000000000000000046711376456024300212410ustar00rootroot00000000000000# frozen_string_literal: false # # httpauth/htgroup.rb -- Apache compatible htgroup file # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: htgroup.rb,v 1.1 2003/02/16 22:22:56 gotoyuzo Exp $ require 'tempfile' module WEBrick module HTTPAuth ## # Htgroup accesses apache-compatible group files. Htgroup can be used to # provide group-based authentication for users. Currently Htgroup is not # directly integrated with any authenticators in WEBrick. For security, # the path for a digest password database should be stored outside of the # paths available to the HTTP server. # # Example: # # htgroup = WEBrick::HTTPAuth::Htgroup.new 'my_group_file' # htgroup.add 'superheroes', %w[spiderman batman] # # htgroup.members('superheroes').include? 'magneto' # => false class Htgroup ## # Open a group database at +path+ def initialize(path) @path = path @mtime = Time.at(0) @group = Hash.new File.open(@path,"a").close unless File.exist?(@path) reload end ## # Reload groups from the database def reload if (mtime = File::mtime(@path)) > @mtime @group.clear File.open(@path){|io| while line = io.gets line.chomp! group, members = line.split(/:\s*/) @group[group] = members.split(/\s+/) end } @mtime = mtime end end ## # Flush the group database. If +output+ is given the database will be # written there instead of to the original path. def flush(output=nil) output ||= @path tmp = Tempfile.create("htgroup", File::dirname(output)) begin @group.keys.sort.each{|group| tmp.puts(format("%s: %s", group, self.members(group).join(" "))) } ensure tmp.close if $! File.unlink(tmp.path) else return File.rename(tmp.path, output) end end end ## # Retrieve the list of members from +group+ def members(group) reload @group[group] || [] end ## # Add an Array of +members+ to +group+ def add(group, members) @group[group] = members(group) | members end end end end webrick-1.7.0/lib/webrick/httpauth/htpasswd.rb000066400000000000000000000111331376456024300213750ustar00rootroot00000000000000# frozen_string_literal: false # # httpauth/htpasswd -- Apache compatible htpasswd file # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $ require_relative 'userdb' require_relative 'basicauth' require 'tempfile' module WEBrick module HTTPAuth ## # Htpasswd accesses apache-compatible password files. Passwords are # matched to a realm where they are valid. For security, the path for a # password database should be stored outside of the paths available to the # HTTP server. # # Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth. # # To create an Htpasswd database with a single user: # # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file' # htpasswd.set_passwd 'my realm', 'username', 'password' # htpasswd.flush class Htpasswd include UserDB ## # Open a password database at +path+ def initialize(path, password_hash: nil) @path = path @mtime = Time.at(0) @passwd = Hash.new @auth_type = BasicAuth @password_hash = password_hash case @password_hash when nil # begin # require "string/crypt" # rescue LoadError # warn("Unable to load string/crypt, proceeding with deprecated use of String#crypt, consider using password_hash: :bcrypt") # end @password_hash = :crypt when :crypt # require "string/crypt" when :bcrypt require "bcrypt" else raise ArgumentError, "only :crypt and :bcrypt are supported for password_hash keyword argument" end File.open(@path,"a").close unless File.exist?(@path) reload end ## # Reload passwords from the database def reload mtime = File::mtime(@path) if mtime > @mtime @passwd.clear File.open(@path){|io| while line = io.gets line.chomp! case line when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z! if @password_hash == :bcrypt raise StandardError, ".htpasswd file contains crypt password, only bcrypt passwords supported" end user, pass = line.split(":") when %r!\A[^:]+:\$2[aby]\$\d{2}\$.{53}\z! if @password_hash == :crypt raise StandardError, ".htpasswd file contains bcrypt password, only crypt passwords supported" end user, pass = line.split(":") when /:\$/, /:{SHA}/ raise NotImplementedError, 'MD5, SHA1 .htpasswd file not supported' else raise StandardError, 'bad .htpasswd file' end @passwd[user] = pass end } @mtime = mtime end end ## # Flush the password database. If +output+ is given the database will # be written there instead of to the original path. def flush(output=nil) output ||= @path tmp = Tempfile.create("htpasswd", File::dirname(output)) renamed = false begin each{|item| tmp.puts(item.join(":")) } tmp.close File::rename(tmp.path, output) renamed = true ensure tmp.close File.unlink(tmp.path) if !renamed end end ## # Retrieves a password from the database for +user+ in +realm+. If # +reload_db+ is true the database will be reloaded first. def get_passwd(realm, user, reload_db) reload() if reload_db @passwd[user] end ## # Sets a password in the database for +user+ in +realm+ to +pass+. def set_passwd(realm, user, pass) if @password_hash == :bcrypt # Cost of 5 to match Apache default, and because the # bcrypt default of 10 will introduce significant delays # for every request. @passwd[user] = BCrypt::Password.create(pass, :cost=>5) else @passwd[user] = make_passwd(realm, user, pass) end end ## # Removes a password from the database for +user+ in +realm+. def delete_passwd(realm, user) @passwd.delete(user) end ## # Iterate passwords in the database. def each # :yields: [user, password] @passwd.keys.sort.each{|user| yield([user, @passwd[user]]) } end end end end webrick-1.7.0/lib/webrick/httpauth/userdb.rb000066400000000000000000000025021376456024300210240ustar00rootroot00000000000000# frozen_string_literal: false #-- # httpauth/userdb.rb -- UserDB mix-in module. # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: userdb.rb,v 1.2 2003/02/20 07:15:48 gotoyuzo Exp $ module WEBrick module HTTPAuth ## # User database mixin for HTTPAuth. This mixin dispatches user record # access to the underlying auth_type for this database. module UserDB ## # The authentication type. # # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are # built-in. attr_accessor :auth_type ## # Creates an obscured password in +realm+ with +user+ and +password+ # using the auth_type of this database. def make_passwd(realm, user, pass) @auth_type::make_passwd(realm, user, pass) end ## # Sets a password in +realm+ with +user+ and +password+ for the # auth_type of this database. def set_passwd(realm, user, pass) self[user] = pass end ## # Retrieves a password in +realm+ for +user+ for the auth_type of this # database. +reload_db+ is a dummy value. def get_passwd(realm, user, reload_db=false) make_passwd(realm, user, self[user]) end end end end webrick-1.7.0/lib/webrick/httpproxy.rb000066400000000000000000000245571376456024300177760ustar00rootroot00000000000000# frozen_string_literal: false # # httpproxy.rb -- HTTPProxy Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2002 GOTO Kentaro # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httpproxy.rb,v 1.18 2003/03/08 18:58:10 gotoyuzo Exp $ # $kNotwork: straw.rb,v 1.3 2002/02/12 15:13:07 gotoken Exp $ require_relative "httpserver" require "net/http" module WEBrick NullReader = Object.new # :nodoc: class << NullReader # :nodoc: def read(*args) nil end alias gets read end FakeProxyURI = Object.new # :nodoc: class << FakeProxyURI # :nodoc: def method_missing(meth, *args) if %w(scheme host port path query userinfo).member?(meth.to_s) return nil end super end end # :startdoc: ## # An HTTP Proxy server which proxies GET, HEAD and POST requests. # # To create a simple proxy server: # # require 'webrick' # require 'webrick/httpproxy' # # proxy = WEBrick::HTTPProxyServer.new Port: 8000 # # trap 'INT' do proxy.shutdown end # trap 'TERM' do proxy.shutdown end # # proxy.start # # See ::new for proxy-specific configuration items. # # == Modifying proxied responses # # To modify content the proxy server returns use the +:ProxyContentHandler+ # option: # # handler = proc do |req, res| # if res['content-type'] == 'text/plain' then # res.body << "\nThis content was proxied!\n" # end # end # # proxy = # WEBrick::HTTPProxyServer.new Port: 8000, ProxyContentHandler: handler class HTTPProxyServer < HTTPServer ## # Proxy server configurations. The proxy server handles the following # configuration items in addition to those supported by HTTPServer: # # :ProxyAuthProc:: Called with a request and response to authorize a # request # :ProxyVia:: Appended to the via header # :ProxyURI:: The proxy server's URI # :ProxyContentHandler:: Called with a request and response and allows # modification of the response # :ProxyTimeout:: Sets the proxy timeouts to 30 seconds for open and 60 # seconds for read operations def initialize(config={}, default=Config::HTTP) super(config, default) c = @config @via = "#{c[:HTTPVersion]} #{c[:ServerName]}:#{c[:Port]}" end # :stopdoc: def service(req, res) if req.request_method == "CONNECT" do_CONNECT(req, res) elsif req.unparsed_uri =~ %r!^http://! proxy_service(req, res) else super(req, res) end end def proxy_auth(req, res) if proc = @config[:ProxyAuthProc] proc.call(req, res) end req.header.delete("proxy-authorization") end def proxy_uri(req, res) # should return upstream proxy server's URI return @config[:ProxyURI] end def proxy_service(req, res) # Proxy Authentication proxy_auth(req, res) begin public_send("do_#{req.request_method}", req, res) rescue NoMethodError raise HTTPStatus::MethodNotAllowed, "unsupported method `#{req.request_method}'." rescue => err logger.debug("#{err.class}: #{err.message}") raise HTTPStatus::ServiceUnavailable, err.message end # Process contents if handler = @config[:ProxyContentHandler] handler.call(req, res) end end def do_CONNECT(req, res) # Proxy Authentication proxy_auth(req, res) ua = Thread.current[:WEBrickSocket] # User-Agent raise HTTPStatus::InternalServerError, "[BUG] cannot get socket" unless ua host, port = req.unparsed_uri.split(":", 2) # Proxy authentication for upstream proxy server if proxy = proxy_uri(req, res) proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0" if proxy.userinfo credentials = "Basic " + [proxy.userinfo].pack("m0") end host, port = proxy.host, proxy.port end begin @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.") os = TCPSocket.new(host, port) # origin server if proxy @logger.debug("CONNECT: sending a Request-Line") os << proxy_request_line << CRLF @logger.debug("CONNECT: > #{proxy_request_line}") if credentials @logger.debug("CONNECT: sending credentials") os << "Proxy-Authorization: " << credentials << CRLF end os << CRLF proxy_status_line = os.gets(LF) @logger.debug("CONNECT: read Status-Line from the upstream server") @logger.debug("CONNECT: < #{proxy_status_line}") if %r{^HTTP/\d+\.\d+\s+200\s*} =~ proxy_status_line while line = os.gets(LF) break if /\A(#{CRLF}|#{LF})\z/om =~ line end else raise HTTPStatus::BadGateway end end @logger.debug("CONNECT #{host}:#{port}: succeeded") res.status = HTTPStatus::RC_OK rescue => ex @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'") res.set_error(ex) raise HTTPStatus::EOFError ensure if handler = @config[:ProxyContentHandler] handler.call(req, res) end res.send_response(ua) access_log(@config, req, res) # Should clear request-line not to send the response twice. # see: HTTPServer#run req.parse(NullReader) rescue nil end begin while fds = IO::select([ua, os]) if fds[0].member?(ua) buf = ua.readpartial(1024); @logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent") os.write(buf) elsif fds[0].member?(os) buf = os.readpartial(1024); @logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}") ua.write(buf) end end rescue os.close @logger.debug("CONNECT #{host}:#{port}: closed") end raise HTTPStatus::EOFError end def do_GET(req, res) perform_proxy_request(req, res, Net::HTTP::Get) end def do_HEAD(req, res) perform_proxy_request(req, res, Net::HTTP::Head) end def do_POST(req, res) perform_proxy_request(req, res, Net::HTTP::Post, req.body_reader) end def do_OPTIONS(req, res) res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT" end private # Some header fields should not be transferred. HopByHop = %w( connection keep-alive proxy-authenticate upgrade proxy-authorization te trailers transfer-encoding ) ShouldNotTransfer = %w( set-cookie proxy-connection ) def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end def choose_header(src, dst) connections = split_field(src['connection']) src.each{|key, value| key = key.downcase if HopByHop.member?(key) || # RFC2616: 13.5.1 connections.member?(key) || # RFC2616: 14.10 ShouldNotTransfer.member?(key) # pragmatics @logger.debug("choose_header: `#{key}: #{value}'") next end dst[key] = value } end # Net::HTTP is stupid about the multiple header fields. # Here is workaround: def set_cookie(src, dst) if str = src['set-cookie'] cookies = [] str.split(/,\s*/).each{|token| if /^[^=]+;/o =~ token cookies[-1] << ", " << token elsif /=/o =~ token cookies << token else cookies[-1] << ", " << token end } dst.cookies.replace(cookies) end end def set_via(h) if @config[:ProxyVia] if h['via'] h['via'] << ", " << @via else h['via'] = @via end end end def setup_proxy_header(req, res) # Choose header fields to transfer header = Hash.new choose_header(req, header) set_via(header) return header end def setup_upstream_proxy_authentication(req, res, header) if upstream = proxy_uri(req, res) if upstream.userinfo header['proxy-authorization'] = "Basic " + [upstream.userinfo].pack("m0") end return upstream end return FakeProxyURI end def create_net_http(uri, upstream) Net::HTTP.new(uri.host, uri.port, upstream.host, upstream.port) end def perform_proxy_request(req, res, req_class, body_stream = nil) uri = req.request_uri path = uri.path.dup path << "?" << uri.query if uri.query header = setup_proxy_header(req, res) upstream = setup_upstream_proxy_authentication(req, res, header) body_tmp = [] http = create_net_http(uri, upstream) req_fib = Fiber.new do http.start do if @config[:ProxyTimeout] ################################## these issues are http.open_timeout = 30 # secs # necessary (maybe because http.read_timeout = 60 # secs # Ruby's bug, but why?) ################################## end if body_stream && req['transfer-encoding'] =~ /\bchunked\b/i header['Transfer-Encoding'] = 'chunked' end http_req = req_class.new(path, header) http_req.body_stream = body_stream if body_stream http.request(http_req) do |response| # Persistent connection requirements are mysterious for me. # So I will close the connection in every response. res['proxy-connection'] = "close" res['connection'] = "close" # stream Net::HTTP::HTTPResponse to WEBrick::HTTPResponse res.status = response.code.to_i res.chunked = response.chunked? choose_header(response, res) set_cookie(response, res) set_via(res) response.read_body do |buf| body_tmp << buf Fiber.yield # wait for res.body Proc#call end end # http.request end end req_fib.resume # read HTTP response headers and first chunk of the body res.body = ->(socket) do while buf = body_tmp.shift socket.write(buf) buf.clear req_fib.resume # continue response.read_body end end end # :stopdoc: end end webrick-1.7.0/lib/webrick/httprequest.rb000066400000000000000000000374701376456024300203030ustar00rootroot00000000000000# frozen_string_literal: false # # httprequest.rb -- HTTPRequest Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httprequest.rb,v 1.64 2003/07/13 17:18:22 gotoyuzo Exp $ require 'fiber' require 'uri' require_relative 'httpversion' require_relative 'httpstatus' require_relative 'httputils' require_relative 'cookie' module WEBrick ## # An HTTP request. This is consumed by service and do_* methods in # WEBrick servlets class HTTPRequest BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ] # :nodoc: # :section: Request line ## # The complete request line such as: # # GET / HTTP/1.1 attr_reader :request_line ## # The request method, GET, POST, PUT, etc. attr_reader :request_method ## # The unparsed URI of the request attr_reader :unparsed_uri ## # The HTTP version of the request attr_reader :http_version # :section: Request-URI ## # The parsed URI of the request attr_reader :request_uri ## # The request path attr_reader :path ## # The script name (CGI variable) attr_accessor :script_name ## # The path info (CGI variable) attr_accessor :path_info ## # The query from the URI of the request attr_accessor :query_string # :section: Header and entity body ## # The raw header of the request attr_reader :raw_header ## # The parsed header of the request attr_reader :header ## # The parsed request cookies attr_reader :cookies ## # The Accept header value attr_reader :accept ## # The Accept-Charset header value attr_reader :accept_charset ## # The Accept-Encoding header value attr_reader :accept_encoding ## # The Accept-Language header value attr_reader :accept_language # :section: ## # The remote user (CGI variable) attr_accessor :user ## # The socket address of the server attr_reader :addr ## # The socket address of the client attr_reader :peeraddr ## # Hash of request attributes attr_reader :attributes ## # Is this a keep-alive connection? attr_reader :keep_alive ## # The local time this request was received attr_reader :request_time ## # Creates a new HTTP request. WEBrick::Config::HTTP is the default # configuration. def initialize(config) @config = config @buffer_size = @config[:InputBufferSize] @logger = config[:Logger] @request_line = @request_method = @unparsed_uri = @http_version = nil @request_uri = @host = @port = @path = nil @script_name = @path_info = nil @query_string = nil @query = nil @form_data = nil @raw_header = Array.new @header = nil @cookies = [] @accept = [] @accept_charset = [] @accept_encoding = [] @accept_language = [] @body = "" @addr = @peeraddr = nil @attributes = {} @user = nil @keep_alive = false @request_time = nil @remaining_size = nil @socket = nil @forwarded_proto = @forwarded_host = @forwarded_port = @forwarded_server = @forwarded_for = nil end ## # Parses a request from +socket+. This is called internally by # WEBrick::HTTPServer. def parse(socket=nil) @socket = socket begin @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : [] @addr = socket.respond_to?(:addr) ? socket.addr : [] rescue Errno::ENOTCONN raise HTTPStatus::EOFError end read_request_line(socket) if @http_version.major > 0 read_header(socket) @header['cookie'].each{|cookie| @cookies += Cookie::parse(cookie) } @accept = HTTPUtils.parse_qvalues(self['accept']) @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset']) @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding']) @accept_language = HTTPUtils.parse_qvalues(self['accept-language']) end return if @request_method == "CONNECT" return if @unparsed_uri == "*" begin setup_forwarded_info @request_uri = parse_uri(@unparsed_uri) @path = HTTPUtils::unescape(@request_uri.path) @path = HTTPUtils::normalize_path(@path) @host = @request_uri.host @port = @request_uri.port @query_string = @request_uri.query @script_name = "" @path_info = @path.dup rescue raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'." end if /\Aclose\z/io =~ self["connection"] @keep_alive = false elsif /\Akeep-alive\z/io =~ self["connection"] @keep_alive = true elsif @http_version < "1.1" @keep_alive = false else @keep_alive = true end end ## # Generate HTTP/1.1 100 continue response if the client expects it, # otherwise does nothing. def continue # :nodoc: if self['expect'] == '100-continue' && @config[:HTTPVersion] >= "1.1" @socket << "HTTP/#{@config[:HTTPVersion]} 100 continue#{CRLF}#{CRLF}" @header.delete('expect') end end ## # Returns the request body. def body(&block) # :yields: body_chunk block ||= Proc.new{|chunk| @body << chunk } read_body(@socket, block) @body.empty? ? nil : @body end ## # Prepares the HTTPRequest object for use as the # source for IO.copy_stream def body_reader @body_tmp = [] @body_rd = Fiber.new do body do |buf| @body_tmp << buf Fiber.yield end end @body_rd.resume # grab the first chunk and yield self end # for IO.copy_stream. def readpartial(size, buf = ''.b) # :nodoc res = @body_tmp.shift or raise EOFError, 'end of file reached' if res.length > size @body_tmp.unshift(res[size..-1]) res = res[0..size - 1] end buf.replace(res) res.clear # get more chunks - check alive? because we can take a partial chunk @body_rd.resume if @body_rd.alive? buf end ## # Request query as a Hash def query unless @query parse_query() end @query end ## # The content-length header def content_length return Integer(self['content-length']) end ## # The content-type header def content_type return self['content-type'] end ## # Retrieves +header_name+ def [](header_name) if @header value = @header[header_name.downcase] value.empty? ? nil : value.join(", ") end end ## # Iterates over the request headers def each if @header @header.each{|k, v| value = @header[k] yield(k, value.empty? ? nil : value.join(", ")) } end end ## # The host this request is for def host return @forwarded_host || @host end ## # The port this request is for def port return @forwarded_port || @port end ## # The server name this request is for def server_name return @forwarded_server || @config[:ServerName] end ## # The client's IP address def remote_ip return self["client-ip"] || @forwarded_for || @peeraddr[3] end ## # Is this an SSL request? def ssl? return @request_uri.scheme == "https" end ## # Should the connection this request was made on be kept alive? def keep_alive? @keep_alive end def to_s # :nodoc: ret = @request_line.dup @raw_header.each{|line| ret << line } ret << CRLF ret << body if body ret end ## # Consumes any remaining body and updates keep-alive status def fixup() # :nodoc: begin body{|chunk| } # read remaining body rescue HTTPStatus::Error => ex @logger.error("HTTPRequest#fixup: #{ex.class} occurred.") @keep_alive = false rescue => ex @logger.error(ex) @keep_alive = false end end # This method provides the metavariables defined by the revision 3 # of "The WWW Common Gateway Interface Version 1.1" # To browse the current document of CGI Version 1.1, see below: # http://tools.ietf.org/html/rfc3875 def meta_vars meta = Hash.new cl = self["Content-Length"] ct = self["Content-Type"] meta["CONTENT_LENGTH"] = cl if cl.to_i > 0 meta["CONTENT_TYPE"] = ct.dup if ct meta["GATEWAY_INTERFACE"] = "CGI/1.1" meta["PATH_INFO"] = @path_info ? @path_info.dup : "" #meta["PATH_TRANSLATED"] = nil # no plan to be provided meta["QUERY_STRING"] = @query_string ? @query_string.dup : "" meta["REMOTE_ADDR"] = @peeraddr[3] meta["REMOTE_HOST"] = @peeraddr[2] #meta["REMOTE_IDENT"] = nil # no plan to be provided meta["REMOTE_USER"] = @user meta["REQUEST_METHOD"] = @request_method.dup meta["REQUEST_URI"] = @request_uri.to_s meta["SCRIPT_NAME"] = @script_name.dup meta["SERVER_NAME"] = @host meta["SERVER_PORT"] = @port.to_s meta["SERVER_PROTOCOL"] = "HTTP/" + @config[:HTTPVersion].to_s meta["SERVER_SOFTWARE"] = @config[:ServerSoftware].dup self.each{|key, val| next if /^content-type$/i =~ key next if /^content-length$/i =~ key name = "HTTP_" + key name.gsub!(/-/o, "_") name.upcase! meta[name] = val } meta end private # :stopdoc: MAX_URI_LENGTH = 2083 # :nodoc: # same as Mongrel, Thin and Puma MAX_HEADER_LENGTH = (112 * 1024) # :nodoc: def read_request_line(socket) @request_line = read_line(socket, MAX_URI_LENGTH) if socket raise HTTPStatus::EOFError unless @request_line @request_bytes = @request_line.bytesize if @request_bytes >= MAX_URI_LENGTH and @request_line[-1, 1] != LF raise HTTPStatus::RequestURITooLarge end @request_time = Time.now if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line @request_method = $1 @unparsed_uri = $2 @http_version = HTTPVersion.new($3 ? $3 : "0.9") else rl = @request_line.sub(/\x0d?\x0a\z/o, '') raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'." end end def read_header(socket) if socket while line = read_line(socket) break if /\A(#{CRLF}|#{LF})\z/om =~ line if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH raise HTTPStatus::RequestEntityTooLarge, 'headers too large' end @raw_header << line end end @header = HTTPUtils::parse_header(@raw_header.join) end def parse_uri(str, scheme="http") if @config[:Escape8bitURI] str = HTTPUtils::escape8bit(str) end str.sub!(%r{\A/+}o, '/') uri = URI::parse(str) return uri if uri.absolute? if @forwarded_host host, port = @forwarded_host, @forwarded_port elsif self["host"] pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n host, port = *self['host'].scan(pattern)[0] elsif @addr.size > 0 host, port = @addr[2], @addr[1] else host, port = @config[:ServerName], @config[:Port] end uri.scheme = @forwarded_proto || scheme uri.host = host uri.port = port ? port.to_i : nil return URI::parse(uri.to_s) end def read_body(socket, block) return unless socket if tc = self['transfer-encoding'] case tc when /\Achunked\z/io then read_chunked(socket, block) else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}." end elsif self['content-length'] || @remaining_size @remaining_size ||= self['content-length'].to_i while @remaining_size > 0 sz = [@buffer_size, @remaining_size].min break unless buf = read_data(socket, sz) @remaining_size -= buf.bytesize block.call(buf) end if @remaining_size > 0 && @socket.eof? raise HTTPStatus::BadRequest, "invalid body size." end elsif BODY_CONTAINABLE_METHODS.member?(@request_method) && !@socket.eof raise HTTPStatus::LengthRequired end return @body end def read_chunk_size(socket) line = read_line(socket) if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line chunk_size = $1.hex chunk_ext = $2 [ chunk_size, chunk_ext ] else raise HTTPStatus::BadRequest, "bad chunk `#{line}'." end end def read_chunked(socket, block) chunk_size, = read_chunk_size(socket) while chunk_size > 0 begin sz = [ chunk_size, @buffer_size ].min data = read_data(socket, sz) # read chunk-data if data.nil? || data.bytesize != sz raise HTTPStatus::BadRequest, "bad chunk data size." end block.call(data) end while (chunk_size -= sz) > 0 read_line(socket) # skip CRLF chunk_size, = read_chunk_size(socket) end read_header(socket) # trailer + CRLF @header.delete("transfer-encoding") @remaining_size = 0 end def _read_data(io, method, *arg) begin WEBrick::Utils.timeout(@config[:RequestTimeout]){ return io.__send__(method, *arg) } rescue Errno::ECONNRESET return nil rescue Timeout::Error raise HTTPStatus::RequestTimeout end end def read_line(io, size=4096) _read_data(io, :gets, LF, size) end def read_data(io, size) _read_data(io, :read, size) end def parse_query() begin if @request_method == "GET" || @request_method == "HEAD" @query = HTTPUtils::parse_query(@query_string) elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/ @query = HTTPUtils::parse_query(body) elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/ boundary = HTTPUtils::dequote($1) @query = HTTPUtils::parse_form_data(body, boundary) else @query = Hash.new end rescue => ex raise HTTPStatus::BadRequest, ex.message end end PrivateNetworkRegexp = / ^unknown$| ^((::ffff:)?127.0.0.1|::1)$| ^(::ffff:)?(10|172\.(1[6-9]|2[0-9]|3[01])|192\.168)\. /ixo # It's said that all X-Forwarded-* headers will contain more than one # (comma-separated) value if the original request already contained one of # these headers. Since we could use these values as Host header, we choose # the initial(first) value. (apr_table_mergen() adds new value after the # existing value with ", " prefix) def setup_forwarded_info if @forwarded_server = self["x-forwarded-server"] @forwarded_server = @forwarded_server.split(",", 2).first end if @forwarded_proto = self["x-forwarded-proto"] @forwarded_proto = @forwarded_proto.split(",", 2).first end if host_port = self["x-forwarded-host"] host_port = host_port.split(",", 2).first if host_port =~ /\A(\[[0-9a-fA-F:]+\])(?::(\d+))?\z/ @forwarded_host = $1 tmp = $2 else @forwarded_host, tmp = host_port.split(":", 2) end @forwarded_port = (tmp || (@forwarded_proto == "https" ? 443 : 80)).to_i end if addrs = self["x-forwarded-for"] addrs = addrs.split(",").collect(&:strip) addrs.reject!{|ip| PrivateNetworkRegexp =~ ip } @forwarded_for = addrs.first end end # :startdoc: end end webrick-1.7.0/lib/webrick/httpresponse.rb000066400000000000000000000323701376456024300204430ustar00rootroot00000000000000# frozen_string_literal: false # # httpresponse.rb -- HTTPResponse Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httpresponse.rb,v 1.45 2003/07/11 11:02:25 gotoyuzo Exp $ require 'time' require 'uri' require_relative 'httpversion' require_relative 'htmlutils' require_relative 'httputils' require_relative 'httpstatus' module WEBrick ## # An HTTP response. This is filled in by the service or do_* methods of a # WEBrick HTTP Servlet. class HTTPResponse class InvalidHeader < StandardError end ## # HTTP Response version attr_reader :http_version ## # Response status code (200) attr_reader :status ## # Response header attr_reader :header ## # Response cookies attr_reader :cookies ## # Response reason phrase ("OK") attr_accessor :reason_phrase ## # Body may be: # * a String; # * an IO-like object that responds to +#read+ and +#readpartial+; # * a Proc-like object that responds to +#call+. # # In the latter case, either #chunked= should be set to +true+, # or header['content-length'] explicitly provided. # Example: # # server.mount_proc '/' do |req, res| # res.chunked = true # # or # # res.header['content-length'] = 10 # res.body = proc { |out| out.write(Time.now.to_s) } # end attr_accessor :body ## # Request method for this response attr_accessor :request_method ## # Request URI for this response attr_accessor :request_uri ## # Request HTTP version for this response attr_accessor :request_http_version ## # Filename of the static file in this response. Only used by the # FileHandler servlet. attr_accessor :filename ## # Is this a keep-alive response? attr_accessor :keep_alive ## # Configuration for this response attr_reader :config ## # Bytes sent in this response attr_reader :sent_size ## # Creates a new HTTP response object. WEBrick::Config::HTTP is the # default configuration. def initialize(config) @config = config @buffer_size = config[:OutputBufferSize] @logger = config[:Logger] @header = Hash.new @status = HTTPStatus::RC_OK @reason_phrase = nil @http_version = HTTPVersion::convert(@config[:HTTPVersion]) @body = '' @keep_alive = true @cookies = [] @request_method = nil @request_uri = nil @request_http_version = @http_version # temporary @chunked = false @filename = nil @sent_size = 0 @bodytempfile = nil end ## # The response's HTTP status line def status_line "HTTP/#@http_version #@status #@reason_phrase".rstrip << CRLF end ## # Sets the response's status to the +status+ code def status=(status) @status = status @reason_phrase = HTTPStatus::reason_phrase(status) end ## # Retrieves the response header +field+ def [](field) @header[field.downcase] end ## # Sets the response header +field+ to +value+ def []=(field, value) @chunked = value.to_s.downcase == 'chunked' if field.downcase == 'transfer-encoding' @header[field.downcase] = value.to_s end ## # The content-length header def content_length if len = self['content-length'] return Integer(len) end end ## # Sets the content-length header to +len+ def content_length=(len) self['content-length'] = len.to_s end ## # The content-type header def content_type self['content-type'] end ## # Sets the content-type header to +type+ def content_type=(type) self['content-type'] = type end ## # Iterates over each header in the response def each @header.each{|field, value| yield(field, value) } end ## # Will this response body be returned using chunked transfer-encoding? def chunked? @chunked end ## # Enables chunked transfer encoding. def chunked=(val) @chunked = val ? true : false end ## # Will this response's connection be kept alive? def keep_alive? @keep_alive end ## # Sends the response on +socket+ def send_response(socket) # :nodoc: begin setup_header() send_header(socket) send_body(socket) rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex @logger.debug(ex) @keep_alive = false rescue Exception => ex @logger.error(ex) @keep_alive = false end end ## # Sets up the headers for sending def setup_header() # :nodoc: @reason_phrase ||= HTTPStatus::reason_phrase(@status) @header['server'] ||= @config[:ServerSoftware] @header['date'] ||= Time.now.httpdate # HTTP/0.9 features if @request_http_version < "1.0" @http_version = HTTPVersion.new("0.9") @keep_alive = false end # HTTP/1.0 features if @request_http_version < "1.1" if chunked? @chunked = false ver = @request_http_version.to_s msg = "chunked is set for an HTTP/#{ver} request. (ignored)" @logger.warn(msg) end end # Determine the message length (RFC2616 -- 4.4 Message Length) if @status == 304 || @status == 204 || HTTPStatus::info?(@status) @header.delete('content-length') @body = "" elsif chunked? @header["transfer-encoding"] = "chunked" @header.delete('content-length') elsif %r{^multipart/byteranges} =~ @header['content-type'] @header.delete('content-length') elsif @header['content-length'].nil? if @body.respond_to? :readpartial elsif @body.respond_to? :call make_body_tempfile else @header['content-length'] = (@body ? @body.bytesize : 0).to_s end end # Keep-Alive connection. if @header['connection'] == "close" @keep_alive = false elsif keep_alive? if chunked? || @header['content-length'] || @status == 304 || @status == 204 || HTTPStatus.info?(@status) @header['connection'] = "Keep-Alive" else msg = "Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true" @logger.warn(msg) @header['connection'] = "close" @keep_alive = false end else @header['connection'] = "close" end # Location is a single absoluteURI. if location = @header['location'] if @request_uri @header['location'] = @request_uri.merge(location).to_s end end end def make_body_tempfile # :nodoc: return if @bodytempfile bodytempfile = Tempfile.create("webrick") if @body.nil? # nothing elsif @body.respond_to? :readpartial IO.copy_stream(@body, bodytempfile) @body.close elsif @body.respond_to? :call @body.call(bodytempfile) else bodytempfile.write @body end bodytempfile.rewind @body = @bodytempfile = bodytempfile @header['content-length'] = bodytempfile.stat.size.to_s end def remove_body_tempfile # :nodoc: if @bodytempfile @bodytempfile.close File.unlink @bodytempfile.path @bodytempfile = nil end end ## # Sends the headers on +socket+ def send_header(socket) # :nodoc: if @http_version.major > 0 data = status_line() @header.each{|key, value| tmp = key.gsub(/\bwww|^te$|\b\w/){ $&.upcase } data << "#{tmp}: #{check_header(value)}" << CRLF } @cookies.each{|cookie| data << "Set-Cookie: " << check_header(cookie.to_s) << CRLF } data << CRLF socket.write(data) end rescue InvalidHeader => e @header.clear @cookies.clear set_error e retry end ## # Sends the body on +socket+ def send_body(socket) # :nodoc: if @body.respond_to? :readpartial then send_body_io(socket) elsif @body.respond_to?(:call) then send_body_proc(socket) else send_body_string(socket) end end ## # Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+. # # Example: # # res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect def set_redirect(status, url) url = URI(url).to_s @body = "#{url}.\n" @header['location'] = url raise status end ## # Creates an error page for exception +ex+ with an optional +backtrace+ def set_error(ex, backtrace=false) case ex when HTTPStatus::Status @keep_alive = false if HTTPStatus::error?(ex.code) self.status = ex.code else @keep_alive = false self.status = HTTPStatus::RC_INTERNAL_SERVER_ERROR end @header['content-type'] = "text/html; charset=ISO-8859-1" if respond_to?(:create_error_page) create_error_page() return end if @request_uri host, port = @request_uri.host, @request_uri.port else host, port = @config[:ServerName], @config[:Port] end error_body(backtrace, ex, host, port) end private def check_header(header_value) header_value = header_value.to_s if /[\r\n]/ =~ header_value raise InvalidHeader else header_value end end # :stopdoc: def error_body(backtrace, ex, host, port) @body = '' @body << <<-_end_of_html_ #{HTMLUtils::escape(@reason_phrase)}

#{HTMLUtils::escape(@reason_phrase)}

#{HTMLUtils::escape(ex.message)}
_end_of_html_ if backtrace && $DEBUG @body << "backtrace of `#{HTMLUtils::escape(ex.class.to_s)}' " @body << "#{HTMLUtils::escape(ex.message)}" @body << "
"
        ex.backtrace.each{|line| @body << "\t#{line}\n"}
        @body << "

" end @body << <<-_end_of_html_
#{HTMLUtils::escape(@config[:ServerSoftware])} at #{host}:#{port}
_end_of_html_ end def send_body_io(socket) begin if @request_method == "HEAD" # do nothing elsif chunked? buf = '' begin @body.readpartial(@buffer_size, buf) size = buf.bytesize data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}" socket.write(data) data.clear @sent_size += size rescue EOFError break end while true buf.clear socket.write("0#{CRLF}#{CRLF}") else if %r{\Abytes (\d+)-(\d+)/\d+\z} =~ @header['content-range'] offset = $1.to_i size = $2.to_i - offset + 1 else offset = nil size = @header['content-length'] size = size.to_i if size end begin @sent_size = IO.copy_stream(@body, socket, size, offset) rescue NotImplementedError @body.seek(offset, IO::SEEK_SET) @sent_size = IO.copy_stream(@body, socket, size) end end ensure @body.close end remove_body_tempfile end def send_body_string(socket) if @request_method == "HEAD" # do nothing elsif chunked? body ? @body.bytesize : 0 while buf = @body[@sent_size, @buffer_size] break if buf.empty? size = buf.bytesize data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}" buf.clear socket.write(data) @sent_size += size end socket.write("0#{CRLF}#{CRLF}") else if @body && @body.bytesize > 0 socket.write(@body) @sent_size = @body.bytesize end end end def send_body_proc(socket) if @request_method == "HEAD" # do nothing elsif chunked? @body.call(ChunkedWrapper.new(socket, self)) socket.write("0#{CRLF}#{CRLF}") else size = @header['content-length'].to_i if @bodytempfile @bodytempfile.rewind IO.copy_stream(@bodytempfile, socket) else @body.call(socket) end @sent_size = size end end class ChunkedWrapper def initialize(socket, resp) @socket = socket @resp = resp end def write(buf) return 0 if buf.empty? socket = @socket @resp.instance_eval { size = buf.bytesize data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}" socket.write(data) data.clear @sent_size += size size } end def <<(*buf) write(buf) self end end # preserved for compatibility with some 3rd-party handlers def _write_data(socket, data) socket << data end # :startdoc: end end webrick-1.7.0/lib/webrick/https.rb000066400000000000000000000061151376456024300170450ustar00rootroot00000000000000# frozen_string_literal: false # # https.rb -- SSL/TLS enhancement for HTTPServer # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2001 GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: https.rb,v 1.15 2003/07/22 19:20:42 gotoyuzo Exp $ require_relative 'ssl' require_relative 'httpserver' module WEBrick module Config HTTP.update(SSL) end ## #-- # Adds SSL functionality to WEBrick::HTTPRequest class HTTPRequest ## # HTTP request SSL cipher attr_reader :cipher ## # HTTP request server certificate attr_reader :server_cert ## # HTTP request client certificate attr_reader :client_cert # :stopdoc: alias orig_parse parse def parse(socket=nil) if socket.respond_to?(:cert) @server_cert = socket.cert || @config[:SSLCertificate] @client_cert = socket.peer_cert @client_cert_chain = socket.peer_cert_chain @cipher = socket.cipher end orig_parse(socket) end alias orig_parse_uri parse_uri def parse_uri(str, scheme="https") if server_cert return orig_parse_uri(str, scheme) end return orig_parse_uri(str) end private :parse_uri alias orig_meta_vars meta_vars def meta_vars meta = orig_meta_vars if server_cert meta["HTTPS"] = "on" meta["SSL_SERVER_CERT"] = @server_cert.to_pem meta["SSL_CLIENT_CERT"] = @client_cert ? @client_cert.to_pem : "" if @client_cert_chain @client_cert_chain.each_with_index{|cert, i| meta["SSL_CLIENT_CERT_CHAIN_#{i}"] = cert.to_pem } end meta["SSL_CIPHER"] = @cipher[0] meta["SSL_PROTOCOL"] = @cipher[1] meta["SSL_CIPHER_USEKEYSIZE"] = @cipher[2].to_s meta["SSL_CIPHER_ALGKEYSIZE"] = @cipher[3].to_s end meta end # :startdoc: end ## #-- # Fake WEBrick::HTTPRequest for lookup_server class SNIRequest ## # The SNI hostname attr_reader :host ## # The socket address of the server attr_reader :addr ## # The port this request is for attr_reader :port ## # Creates a new SNIRequest. def initialize(sslsocket, hostname) @host = hostname @addr = sslsocket.addr @port = @addr[1] end end ## #-- # Adds SSL functionality to WEBrick::HTTPServer class HTTPServer < ::WEBrick::GenericServer ## # ServerNameIndication callback def ssl_servername_callback(sslsocket, hostname = nil) req = SNIRequest.new(sslsocket, hostname) server = lookup_server(req) server ? server.ssl_context : nil end # :stopdoc: ## # Check whether +server+ is also SSL server. # Also +server+'s SSL context will be created. alias orig_virtual_host virtual_host def virtual_host(server) if @config[:SSLEnable] && !server.ssl_context raise ArgumentError, "virtual host must set SSLEnable to true" end orig_virtual_host(server) end # :startdoc: end end webrick-1.7.0/lib/webrick/httpserver.rb000066400000000000000000000202571376456024300201140ustar00rootroot00000000000000# frozen_string_literal: false # # httpserver.rb -- HTTPServer Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $ require 'io/wait' require_relative 'server' require_relative 'httputils' require_relative 'httpstatus' require_relative 'httprequest' require_relative 'httpresponse' require_relative 'httpservlet' require_relative 'accesslog' module WEBrick class HTTPServerError < ServerError; end ## # An HTTP Server class HTTPServer < ::WEBrick::GenericServer ## # Creates a new HTTP server according to +config+ # # An HTTP server uses the following attributes: # # :AccessLog:: An array of access logs. See WEBrick::AccessLog # :BindAddress:: Local address for the server to bind to # :DocumentRoot:: Root path to serve files from # :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler # :HTTPVersion:: The HTTP version of this server # :Port:: Port to listen on # :RequestCallback:: Called with a request and response before each # request is serviced. # :RequestTimeout:: Maximum time to wait between requests # :ServerAlias:: Array of alternate names for this server for virtual # hosting # :ServerName:: Name for this server for virtual hosting def initialize(config={}, default=Config::HTTP) super(config, default) @http_version = HTTPVersion::convert(@config[:HTTPVersion]) @mount_tab = MountTable.new if @config[:DocumentRoot] mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot], @config[:DocumentRootOptions]) end unless @config[:AccessLog] @config[:AccessLog] = [ [ $stderr, AccessLog::COMMON_LOG_FORMAT ], [ $stderr, AccessLog::REFERER_LOG_FORMAT ] ] end @virtual_hosts = Array.new end ## # Processes requests on +sock+ def run(sock) while true req = create_request(@config) res = create_response(@config) server = self begin timeout = @config[:RequestTimeout] while timeout > 0 break if sock.to_io.wait_readable(0.5) break if @status != :Running timeout -= 0.5 end raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running raise HTTPStatus::EOFError if sock.eof? req.parse(sock) res.request_method = req.request_method res.request_uri = req.request_uri res.request_http_version = req.http_version res.keep_alive = req.keep_alive? server = lookup_server(req) || self if callback = server[:RequestCallback] callback.call(req, res) elsif callback = server[:RequestHandler] msg = ":RequestHandler is deprecated, please use :RequestCallback" @logger.warn(msg) callback.call(req, res) end server.service(req, res) rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex res.set_error(ex) rescue HTTPStatus::Error => ex @logger.error(ex.message) res.set_error(ex) rescue HTTPStatus::Status => ex res.status = ex.code rescue StandardError => ex @logger.error(ex) res.set_error(ex, true) ensure if req.request_line if req.keep_alive? && res.keep_alive? req.fixup() end res.send_response(sock) server.access_log(@config, req, res) end end break if @http_version < "1.1" break unless req.keep_alive? break unless res.keep_alive? end end ## # Services +req+ and fills in +res+ def service(req, res) if req.unparsed_uri == "*" if req.request_method == "OPTIONS" do_OPTIONS(req, res) raise HTTPStatus::OK end raise HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found." end servlet, options, script_name, path_info = search_servlet(req.path) raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet req.script_name = script_name req.path_info = path_info si = servlet.get_instance(self, *options) @logger.debug(format("%s is invoked.", si.class.name)) si.service(req, res) end ## # The default OPTIONS request handler says GET, HEAD, POST and OPTIONS # requests are allowed. def do_OPTIONS(req, res) res["allow"] = "GET,HEAD,POST,OPTIONS" end ## # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation # time def mount(dir, servlet, *options) @logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir)) @mount_tab[dir] = [ servlet, options ] end ## # Mounts +proc+ or +block+ on +dir+ and calls it with a # WEBrick::HTTPRequest and WEBrick::HTTPResponse def mount_proc(dir, proc=nil, &block) proc ||= block raise HTTPServerError, "must pass a proc or block" unless proc mount(dir, HTTPServlet::ProcHandler.new(proc)) end ## # Unmounts +dir+ def unmount(dir) @logger.debug(sprintf("unmount %s.", dir)) @mount_tab.delete(dir) end alias umount unmount ## # Finds a servlet for +path+ def search_servlet(path) script_name, path_info = @mount_tab.scan(path) servlet, options = @mount_tab[script_name] if servlet [ servlet, options, script_name, path_info ] end end ## # Adds +server+ as a virtual host. def virtual_host(server) @virtual_hosts << server @virtual_hosts = @virtual_hosts.sort_by{|s| num = 0 num -= 4 if s[:BindAddress] num -= 2 if s[:Port] num -= 1 if s[:ServerName] num } end ## # Finds the appropriate virtual host to handle +req+ def lookup_server(req) @virtual_hosts.find{|s| (s[:BindAddress].nil? || req.addr[3] == s[:BindAddress]) && (s[:Port].nil? || req.port == s[:Port]) && ((s[:ServerName].nil? || req.host == s[:ServerName]) || (!s[:ServerAlias].nil? && s[:ServerAlias].find{|h| h === req.host})) } end ## # Logs +req+ and +res+ in the access logs. +config+ is used for the # server name. def access_log(config, req, res) param = AccessLog::setup_params(config, req, res) @config[:AccessLog].each{|logger, fmt| logger << AccessLog::format(fmt+"\n", param) } end ## # Creates the HTTPRequest used when handling the HTTP # request. Can be overridden by subclasses. def create_request(with_webrick_config) HTTPRequest.new(with_webrick_config) end ## # Creates the HTTPResponse used when handling the HTTP # request. Can be overridden by subclasses. def create_response(with_webrick_config) HTTPResponse.new(with_webrick_config) end ## # Mount table for the path a servlet is mounted on in the directory space # of the server. Users of WEBrick can only access this indirectly via # WEBrick::HTTPServer#mount, WEBrick::HTTPServer#unmount and # WEBrick::HTTPServer#search_servlet class MountTable # :nodoc: def initialize @tab = Hash.new compile end def [](dir) dir = normalize(dir) @tab[dir] end def []=(dir, val) dir = normalize(dir) @tab[dir] = val compile val end def delete(dir) dir = normalize(dir) res = @tab.delete(dir) compile res end def scan(path) @scanner =~ path [ $&, $' ] end private def compile k = @tab.keys k.sort! k.reverse! k.collect!{|path| Regexp.escape(path) } @scanner = Regexp.new("\\A(" + k.join("|") +")(?=/|\\z)") end def normalize(dir) ret = dir ? dir.dup : "" ret.sub!(%r|/+\z|, "") ret end end end end webrick-1.7.0/lib/webrick/httpservlet.rb000066400000000000000000000013011376456024300202570ustar00rootroot00000000000000# frozen_string_literal: false # # httpservlet.rb -- HTTPServlet Utility File # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httpservlet.rb,v 1.21 2003/02/23 12:24:46 gotoyuzo Exp $ require_relative 'httpservlet/abstract' require_relative 'httpservlet/filehandler' require_relative 'httpservlet/cgihandler' require_relative 'httpservlet/erbhandler' require_relative 'httpservlet/prochandler' module WEBrick module HTTPServlet FileHandler.add_handler("cgi", CGIHandler) FileHandler.add_handler("rhtml", ERBHandler) end end webrick-1.7.0/lib/webrick/httpservlet/000077500000000000000000000000001376456024300177375ustar00rootroot00000000000000webrick-1.7.0/lib/webrick/httpservlet/abstract.rb000066400000000000000000000103101376456024300220620ustar00rootroot00000000000000# frozen_string_literal: false # # httpservlet.rb -- HTTPServlet Module # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: abstract.rb,v 1.24 2003/07/11 11:16:46 gotoyuzo Exp $ require_relative '../htmlutils' require_relative '../httputils' require_relative '../httpstatus' module WEBrick module HTTPServlet class HTTPServletError < StandardError; end ## # AbstractServlet allows HTTP server modules to be reused across multiple # servers and allows encapsulation of functionality. # # By default a servlet will respond to GET, HEAD (through an alias to GET) # and OPTIONS requests. # # By default a new servlet is initialized for every request. A servlet # instance can be reused by overriding ::get_instance in the # AbstractServlet subclass. # # == A Simple Servlet # # class Simple < WEBrick::HTTPServlet::AbstractServlet # def do_GET request, response # status, content_type, body = do_stuff_with request # # response.status = status # response['Content-Type'] = content_type # response.body = body # end # # def do_stuff_with request # return 200, 'text/plain', 'you got a page' # end # end # # This servlet can be mounted on a server at a given path: # # server.mount '/simple', Simple # # == Servlet Configuration # # Servlets can be configured via initialize. The first argument is the # HTTP server the servlet is being initialized for. # # class Configurable < Simple # def initialize server, color, size # super server # @color = color # @size = size # end # # def do_stuff_with request # content = "

Hello, World!" # # return 200, "text/html", content # end # end # # This servlet must be provided two arguments at mount time: # # server.mount '/configurable', Configurable, 'red', '2em' class AbstractServlet ## # Factory for servlet instances that will handle a request from +server+ # using +options+ from the mount point. By default a new servlet # instance is created for every call. def self.get_instance(server, *options) self.new(server, *options) end ## # Initializes a new servlet for +server+ using +options+ which are # stored as-is in +@options+. +@logger+ is also provided. def initialize(server, *options) @server = @config = server @logger = @server[:Logger] @options = options end ## # Dispatches to a +do_+ method based on +req+ if such a method is # available. (+do_GET+ for a GET request). Raises a MethodNotAllowed # exception if the method is not implemented. def service(req, res) method_name = "do_" + req.request_method.gsub(/-/, "_") if respond_to?(method_name) __send__(method_name, req, res) else raise HTTPStatus::MethodNotAllowed, "unsupported method `#{req.request_method}'." end end ## # Raises a NotFound exception def do_GET(req, res) raise HTTPStatus::NotFound, "not found." end ## # Dispatches to do_GET def do_HEAD(req, res) do_GET(req, res) end ## # Returns the allowed HTTP request methods def do_OPTIONS(req, res) m = self.methods.grep(/\Ado_([A-Z]+)\z/) {$1} m.sort! res["allow"] = m.join(",") end private ## # Redirects to a path ending in / def redirect_to_directory_uri(req, res) if req.path[-1] != ?/ location = WEBrick::HTTPUtils.escape_path(req.path + "/") if req.query_string && req.query_string.bytesize > 0 location << "?" << req.query_string end res.set_redirect(HTTPStatus::MovedPermanently, location) end end end end end webrick-1.7.0/lib/webrick/httpservlet/cgi_runner.rb000066400000000000000000000017671376456024300224320ustar00rootroot00000000000000# frozen_string_literal: false # # cgi_runner.rb -- CGI launcher. # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: cgi_runner.rb,v 1.9 2002/09/25 11:33:15 gotoyuzo Exp $ def sysread(io, size) buf = "" while size > 0 tmp = io.sysread(size) buf << tmp size -= tmp.bytesize end return buf end STDIN.binmode len = sysread(STDIN, 8).to_i out = sysread(STDIN, len) STDOUT.reopen(File.open(out, "w")) len = sysread(STDIN, 8).to_i err = sysread(STDIN, len) STDERR.reopen(File.open(err, "w")) len = sysread(STDIN, 8).to_i dump = sysread(STDIN, len) hash = Marshal.restore(dump) ENV.keys.each{|name| ENV.delete(name) } hash.each{|k, v| ENV[k] = v if v } dir = File::dirname(ENV["SCRIPT_FILENAME"]) Dir::chdir dir if ARGV[0] argv = ARGV.dup argv << ENV["SCRIPT_FILENAME"] exec(*argv) # NOTREACHED end exec ENV["SCRIPT_FILENAME"] webrick-1.7.0/lib/webrick/httpservlet/cgihandler.rb000066400000000000000000000075161376456024300223750ustar00rootroot00000000000000# frozen_string_literal: false # # cgihandler.rb -- CGIHandler Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: cgihandler.rb,v 1.27 2003/03/21 19:56:01 gotoyuzo Exp $ require 'rbconfig' require 'tempfile' require_relative '../config' require_relative 'abstract' module WEBrick module HTTPServlet ## # Servlet for handling CGI scripts # # Example: # # server.mount('/cgi/my_script', WEBrick::HTTPServlet::CGIHandler, # '/path/to/my_script') class CGIHandler < AbstractServlet Ruby = RbConfig.ruby # :nodoc: CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc: CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb".freeze].freeze # :nodoc: ## # Creates a new CGI script servlet for the script at +name+ def initialize(server, name) super(server, name) @script_filename = name @tempdir = server[:TempDir] interpreter = server[:CGIInterpreter] if interpreter.is_a?(Array) @cgicmd = CGIRunnerArray + interpreter else @cgicmd = "#{CGIRunner} #{interpreter}" end end # :stopdoc: def do_GET(req, res) cgi_in = IO::popen(@cgicmd, "wb") cgi_out = Tempfile.new("webrick.cgiout.", @tempdir, mode: IO::BINARY) cgi_out.set_encoding("ASCII-8BIT") cgi_err = Tempfile.new("webrick.cgierr.", @tempdir, mode: IO::BINARY) cgi_err.set_encoding("ASCII-8BIT") begin cgi_in.sync = true meta = req.meta_vars meta["SCRIPT_FILENAME"] = @script_filename meta["PATH"] = @config[:CGIPathEnv] meta.delete("HTTP_PROXY") if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM meta["SystemRoot"] = ENV["SystemRoot"] end dump = Marshal.dump(meta) cgi_in.write("%8d" % cgi_out.path.bytesize) cgi_in.write(cgi_out.path) cgi_in.write("%8d" % cgi_err.path.bytesize) cgi_in.write(cgi_err.path) cgi_in.write("%8d" % dump.bytesize) cgi_in.write(dump) req.body { |chunk| cgi_in.write(chunk) } ensure cgi_in.close status = $?.exitstatus sleep 0.1 if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM data = cgi_out.read cgi_out.close(true) if errmsg = cgi_err.read if errmsg.bytesize > 0 @logger.error("CGIHandler: #{@script_filename}:\n" + errmsg) end end cgi_err.close(true) end if status != 0 @logger.error("CGIHandler: #{@script_filename} exit with #{status}") end data = "" unless data raw_header, body = data.split(/^[\xd\xa]+/, 2) raise HTTPStatus::InternalServerError, "Premature end of script headers: #{@script_filename}" if body.nil? begin header = HTTPUtils::parse_header(raw_header) if /^(\d+)/ =~ header['status'][0] res.status = $1.to_i header.delete('status') end if header.has_key?('location') # RFC 3875 6.2.3, 6.2.4 res.status = 302 unless (300...400) === res.status end if header.has_key?('set-cookie') header['set-cookie'].each{|k| res.cookies << Cookie.parse_set_cookie(k) } header.delete('set-cookie') end header.each{|key, val| res[key] = val.join(", ") } rescue => ex raise HTTPStatus::InternalServerError, ex.message end res.body = body end alias do_POST do_GET # :startdoc: end end end webrick-1.7.0/lib/webrick/httpservlet/erbhandler.rb000066400000000000000000000044311376456024300223740ustar00rootroot00000000000000# frozen_string_literal: false # # erbhandler.rb -- ERBHandler Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: erbhandler.rb,v 1.25 2003/02/24 19:25:31 gotoyuzo Exp $ require_relative 'abstract' require 'erb' module WEBrick module HTTPServlet ## # ERBHandler evaluates an ERB file and returns the result. This handler # is automatically used if there are .rhtml files in a directory served by # the FileHandler. # # ERBHandler supports GET and POST methods. # # The ERB file is evaluated with the local variables +servlet_request+ and # +servlet_response+ which are a WEBrick::HTTPRequest and # WEBrick::HTTPResponse respectively. # # Example .rhtml file: # # Request to <%= servlet_request.request_uri %> # # Query params <%= servlet_request.query.inspect %> class ERBHandler < AbstractServlet ## # Creates a new ERBHandler on +server+ that will evaluate and serve the # ERB file +name+ def initialize(server, name) super(server, name) @script_filename = name end ## # Handles GET requests def do_GET(req, res) unless defined?(ERB) @logger.warn "#{self.class}: ERB not defined." raise HTTPStatus::Forbidden, "ERBHandler cannot work." end begin data = File.open(@script_filename, &:read) res.body = evaluate(ERB.new(data), req, res) res['content-type'] ||= HTTPUtils::mime_type(@script_filename, @config[:MimeTypes]) rescue StandardError raise rescue Exception => ex @logger.error(ex) raise HTTPStatus::InternalServerError, ex.message end end ## # Handles POST requests alias do_POST do_GET private ## # Evaluates +erb+ providing +servlet_request+ and +servlet_response+ as # local variables. def evaluate(erb, servlet_request, servlet_response) Module.new.module_eval{ servlet_request.meta_vars servlet_request.query erb.result(binding) } end end end end webrick-1.7.0/lib/webrick/httpservlet/filehandler.rb000066400000000000000000000424621376456024300225510ustar00rootroot00000000000000# frozen_string_literal: false # # filehandler.rb -- FileHandler Module # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $ require 'time' require_relative '../htmlutils' require_relative '../httputils' require_relative '../httpstatus' module WEBrick module HTTPServlet ## # Servlet for serving a single file. You probably want to use the # FileHandler servlet instead as it handles directories and fancy indexes. # # Example: # # server.mount('/my_page.txt', WEBrick::HTTPServlet::DefaultFileHandler, # '/path/to/my_page.txt') # # This servlet handles If-Modified-Since and Range requests. class DefaultFileHandler < AbstractServlet ## # Creates a DefaultFileHandler instance for the file at +local_path+. def initialize(server, local_path) super(server, local_path) @local_path = local_path end # :stopdoc: def do_GET(req, res) st = File::stat(@local_path) mtime = st.mtime res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i) if not_modified?(req, res, mtime, res['etag']) res.body = '' raise HTTPStatus::NotModified elsif req['range'] make_partial_content(req, res, @local_path, st.size) raise HTTPStatus::PartialContent else mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes]) res['content-type'] = mtype res['content-length'] = st.size.to_s res['last-modified'] = mtime.httpdate res.body = File.open(@local_path, "rb") end end def not_modified?(req, res, mtime, etag) if ir = req['if-range'] begin if Time.httpdate(ir) >= mtime return true end rescue if HTTPUtils::split_header_value(ir).member?(res['etag']) return true end end end if (ims = req['if-modified-since']) && Time.parse(ims) >= mtime return true end if (inm = req['if-none-match']) && HTTPUtils::split_header_value(inm).member?(res['etag']) return true end return false end # returns a lambda for webrick/httpresponse.rb send_body_proc def multipart_body(body, parts, boundary, mtype, filesize) lambda do |socket| begin begin first = parts.shift last = parts.shift socket.write( "--#{boundary}#{CRLF}" \ "Content-Type: #{mtype}#{CRLF}" \ "Content-Range: bytes #{first}-#{last}/#{filesize}#{CRLF}" \ "#{CRLF}" ) begin IO.copy_stream(body, socket, last - first + 1, first) rescue NotImplementedError body.seek(first, IO::SEEK_SET) IO.copy_stream(body, socket, last - first + 1) end socket.write(CRLF) end while parts[0] socket.write("--#{boundary}--#{CRLF}") ensure body.close end end end def make_partial_content(req, res, filename, filesize) mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes]) unless ranges = HTTPUtils::parse_range_header(req['range']) raise HTTPStatus::BadRequest, "Unrecognized range-spec: \"#{req['range']}\"" end File.open(filename, "rb"){|io| if ranges.size > 1 time = Time.now boundary = "#{time.sec}_#{time.usec}_#{Process::pid}" parts = [] ranges.each {|range| prange = prepare_range(range, filesize) next if prange[0] < 0 parts.concat(prange) } raise HTTPStatus::RequestRangeNotSatisfiable if parts.empty? res["content-type"] = "multipart/byteranges; boundary=#{boundary}" if req.http_version < '1.1' res['connection'] = 'close' else res.chunked = true end res.body = multipart_body(io.dup, parts, boundary, mtype, filesize) elsif range = ranges[0] first, last = prepare_range(range, filesize) raise HTTPStatus::RequestRangeNotSatisfiable if first < 0 res['content-type'] = mtype res['content-range'] = "bytes #{first}-#{last}/#{filesize}" res['content-length'] = (last - first + 1).to_s res.body = io.dup else raise HTTPStatus::BadRequest end } end def prepare_range(range, filesize) first = range.first < 0 ? filesize + range.first : range.first return -1, -1 if first < 0 || first >= filesize last = range.last < 0 ? filesize + range.last : range.last last = filesize - 1 if last >= filesize return first, last end # :startdoc: end ## # Serves a directory including fancy indexing and a variety of other # options. # # Example: # # server.mount('/assets', WEBrick::HTTPServlet::FileHandler, # '/path/to/assets') class FileHandler < AbstractServlet HandlerTable = Hash.new # :nodoc: ## # Allow custom handling of requests for files with +suffix+ by class # +handler+ def self.add_handler(suffix, handler) HandlerTable[suffix] = handler end ## # Remove custom handling of requests for files with +suffix+ def self.remove_handler(suffix) HandlerTable.delete(suffix) end ## # Creates a FileHandler servlet on +server+ that serves files starting # at directory +root+ # # +options+ may be a Hash containing keys from # WEBrick::Config::FileHandler or +true+ or +false+. # # If +options+ is true or false then +:FancyIndexing+ is enabled or # disabled respectively. def initialize(server, root, options={}, default=Config::FileHandler) @config = server.config @logger = @config[:Logger] @root = File.expand_path(root) if options == true || options == false options = { :FancyIndexing => options } end @options = default.dup.update(options) end # :stopdoc: def set_filesystem_encoding(str) enc = Encoding.find('filesystem') if enc == Encoding::US_ASCII str.b else str.dup.force_encoding(enc) end end def service(req, res) # if this class is mounted on "/" and /~username is requested. # we're going to override path information before invoking service. if defined?(Etc) && @options[:UserDir] && req.script_name.empty? if %r|^(/~([^/]+))| =~ req.path_info script_name, user = $1, $2 path_info = $' begin passwd = Etc::getpwnam(user) @root = File::join(passwd.dir, @options[:UserDir]) req.script_name = script_name req.path_info = path_info rescue @logger.debug "#{self.class}#do_GET: getpwnam(#{user}) failed" end end end prevent_directory_traversal(req, res) super(req, res) end def do_GET(req, res) unless exec_handler(req, res) set_dir_list(req, res) end end def do_POST(req, res) unless exec_handler(req, res) raise HTTPStatus::NotFound, "`#{req.path}' not found." end end def do_OPTIONS(req, res) unless exec_handler(req, res) super(req, res) end end # ToDo # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV # # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE # LOCK UNLOCK # RFC3253: Versioning Extensions to WebDAV # (Web Distributed Authoring and Versioning) # # VERSION-CONTROL REPORT CHECKOUT CHECK_IN UNCHECKOUT # MKWORKSPACE UPDATE LABEL MERGE ACTIVITY private def trailing_pathsep?(path) # check for trailing path separator: # File.dirname("/aaaa/bbbb/") #=> "/aaaa") # File.dirname("/aaaa/bbbb/x") #=> "/aaaa/bbbb") # File.dirname("/aaaa/bbbb") #=> "/aaaa") # File.dirname("/aaaa/bbbbx") #=> "/aaaa") return File.dirname(path) != File.dirname(path+"x") end def prevent_directory_traversal(req, res) # Preventing directory traversal on Windows platforms; # Backslashes (0x5c) in path_info are not interpreted as special # character in URI notation. So the value of path_info should be # normalize before accessing to the filesystem. # dirty hack for filesystem encoding; in nature, File.expand_path # should not be used for path normalization. [Bug #3345] path = req.path_info.dup.force_encoding(Encoding.find("filesystem")) if trailing_pathsep?(req.path_info) # File.expand_path removes the trailing path separator. # Adding a character is a workaround to save it. # File.expand_path("/aaa/") #=> "/aaa" # File.expand_path("/aaa/" + "x") #=> "/aaa/x" expanded = File.expand_path(path + "x") expanded.chop! # remove trailing "x" else expanded = File.expand_path(path) end expanded.force_encoding(req.path_info.encoding) req.path_info = expanded end def exec_handler(req, res) raise HTTPStatus::NotFound, "`#{req.path}' not found." unless @root if set_filename(req, res) handler = get_handler(req, res) call_callback(:HandlerCallback, req, res) h = handler.get_instance(@config, res.filename) h.service(req, res) return true end call_callback(:HandlerCallback, req, res) return false end def get_handler(req, res) suffix1 = (/\.(\w+)\z/ =~ res.filename) && $1.downcase if /\.(\w+)\.([\w\-]+)\z/ =~ res.filename if @options[:AcceptableLanguages].include?($2.downcase) suffix2 = $1.downcase end end handler_table = @options[:HandlerTable] return handler_table[suffix1] || handler_table[suffix2] || HandlerTable[suffix1] || HandlerTable[suffix2] || DefaultFileHandler end def set_filename(req, res) res.filename = @root path_info = req.path_info.scan(%r|/[^/]*|) path_info.unshift("") # dummy for checking @root dir while base = path_info.first base = set_filesystem_encoding(base) break if base == "/" break unless File.directory?(File.expand_path(res.filename + base)) shift_path_info(req, res, path_info) call_callback(:DirectoryCallback, req, res) end if base = path_info.first base = set_filesystem_encoding(base) if base == "/" if file = search_index_file(req, res) shift_path_info(req, res, path_info, file) call_callback(:FileCallback, req, res) return true end shift_path_info(req, res, path_info) elsif file = search_file(req, res, base) shift_path_info(req, res, path_info, file) call_callback(:FileCallback, req, res) return true else raise HTTPStatus::NotFound, "`#{req.path}' not found." end end return false end def check_filename(req, res, name) if nondisclosure_name?(name) || windows_ambiguous_name?(name) @logger.warn("the request refers nondisclosure name `#{name}'.") raise HTTPStatus::NotFound, "`#{req.path}' not found." end end def shift_path_info(req, res, path_info, base=nil) tmp = path_info.shift base = base || set_filesystem_encoding(tmp) req.path_info = path_info.join req.script_name << base res.filename = File.expand_path(res.filename + base) check_filename(req, res, File.basename(res.filename)) end def search_index_file(req, res) @config[:DirectoryIndex].each{|index| if file = search_file(req, res, "/"+index) return file end } return nil end def search_file(req, res, basename) langs = @options[:AcceptableLanguages] path = res.filename + basename if File.file?(path) return basename elsif langs.size > 0 req.accept_language.each{|lang| path_with_lang = path + ".#{lang}" if langs.member?(lang) && File.file?(path_with_lang) return basename + ".#{lang}" end } (langs - req.accept_language).each{|lang| path_with_lang = path + ".#{lang}" if File.file?(path_with_lang) return basename + ".#{lang}" end } end return nil end def call_callback(callback_name, req, res) if cb = @options[callback_name] cb.call(req, res) end end def windows_ambiguous_name?(name) return true if /[. ]+\z/ =~ name return true if /::\$DATA\z/ =~ name return false end def nondisclosure_name?(name) @options[:NondisclosureName].each{|pattern| if File.fnmatch(pattern, name, File::FNM_CASEFOLD) return true end } return false end def set_dir_list(req, res) redirect_to_directory_uri(req, res) unless @options[:FancyIndexing] raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'" end local_path = res.filename list = Dir::entries(local_path).collect{|name| next if name == "." || name == ".." next if nondisclosure_name?(name) next if windows_ambiguous_name?(name) st = (File::stat(File.join(local_path, name)) rescue nil) if st.nil? [ name, nil, -1 ] elsif st.directory? [ name + "/", st.mtime, -1 ] else [ name, st.mtime, st.size ] end } list.compact! query = req.query d0 = nil idx = nil %w[N M S].each_with_index do |q, i| if d = query.delete(q) idx ||= i d0 ||= d end end d0 ||= "A" idx ||= 0 d1 = (d0 == "A") ? "D" : "A" if d0 == "A" list.sort!{|a,b| a[idx] <=> b[idx] } else list.sort!{|a,b| b[idx] <=> a[idx] } end namewidth = query["NameWidth"] if namewidth == "*" namewidth = nil elsif !namewidth or (namewidth = namewidth.to_i) < 2 namewidth = 25 end query = query.inject('') {|s, (k, v)| s << '&' << HTMLUtils::escape("#{k}=#{v}")} type = "text/html" case enc = Encoding.find('filesystem') when Encoding::US_ASCII, Encoding::ASCII_8BIT else type << "; charset=\"#{enc.name}\"" end res['content-type'] = type title = "Index of #{HTMLUtils::escape(req.path)}" res.body = <<-_end_of_html_ #{title}

#{title}

_end_of_html_ res.body << "\n" res.body << "" res.body << "" res.body << "\n" res.body << "\n" res.body << "\n" query.sub!(/\A&/, '?') list.unshift [ "..", File::mtime(local_path+"/.."), -1 ] list.each{ |name, time, size| if name == ".." dname = "Parent Directory" elsif namewidth and name.size > namewidth dname = name[0...(namewidth - 2)] << '..' else dname = name end s = "" s << "" s << "\n" res.body << s } res.body << "
NameLast modifiedSize
#{HTMLUtils::escape(dname)}" << (time ? time.strftime("%Y/%m/%d %H:%M") : "") << "" << (size >= 0 ? size.to_s : "-") << "
" res.body << "
" res.body << <<-_end_of_html_
#{HTMLUtils::escape(@config[:ServerSoftware])}
at #{req.host}:#{req.port}
_end_of_html_ end # :startdoc: end end end webrick-1.7.0/lib/webrick/httpservlet/prochandler.rb000066400000000000000000000020341376456024300225640ustar00rootroot00000000000000# frozen_string_literal: false # # prochandler.rb -- ProcHandler Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: prochandler.rb,v 1.7 2002/09/21 12:23:42 gotoyuzo Exp $ require_relative 'abstract' module WEBrick module HTTPServlet ## # Mounts a proc at a path that accepts a request and response. # # Instead of mounting this servlet with WEBrick::HTTPServer#mount use # WEBrick::HTTPServer#mount_proc: # # server.mount_proc '/' do |req, res| # res.body = 'it worked!' # res.status = 200 # end class ProcHandler < AbstractServlet # :stopdoc: def get_instance(server, *options) self end def initialize(proc) @proc = proc end def do_GET(request, response) @proc.call(request, response) end alias do_POST do_GET # :startdoc: end end end webrick-1.7.0/lib/webrick/httpstatus.rb000066400000000000000000000123651376456024300201320ustar00rootroot00000000000000# frozen_string_literal: false #-- # httpstatus.rb -- HTTPStatus Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httpstatus.rb,v 1.11 2003/03/24 20:18:55 gotoyuzo Exp $ require_relative 'accesslog' module WEBrick ## # This module is used to manager HTTP status codes. # # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for more # information. module HTTPStatus ## # Root of the HTTP status class hierarchy class Status < StandardError class << self attr_reader :code, :reason_phrase # :nodoc: end # Returns the HTTP status code def code() self::class::code end # Returns the HTTP status description def reason_phrase() self::class::reason_phrase end alias to_i code # :nodoc: end # Root of the HTTP info statuses class Info < Status; end # Root of the HTTP success statuses class Success < Status; end # Root of the HTTP redirect statuses class Redirect < Status; end # Root of the HTTP error statuses class Error < Status; end # Root of the HTTP client error statuses class ClientError < Error; end # Root of the HTTP server error statuses class ServerError < Error; end class EOFError < StandardError; end # HTTP status codes and descriptions StatusMessage = { # :nodoc: 100 => 'Continue', 101 => 'Switching Protocols', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Request Range Not Satisfiable', 417 => 'Expectation Failed', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 507 => 'Insufficient Storage', 511 => 'Network Authentication Required', } # Maps a status code to the corresponding Status class CodeToError = {} # :nodoc: # Creates a status or error class for each status code and # populates the CodeToError map. StatusMessage.each{|code, message| message.freeze var_name = message.gsub(/[ \-]/,'_').upcase err_name = message.gsub(/[ \-]/,'') case code when 100...200; parent = Info when 200...300; parent = Success when 300...400; parent = Redirect when 400...500; parent = ClientError when 500...600; parent = ServerError end const_set("RC_#{var_name}", code) err_class = Class.new(parent) err_class.instance_variable_set(:@code, code) err_class.instance_variable_set(:@reason_phrase, message) const_set(err_name, err_class) CodeToError[code] = err_class } ## # Returns the description corresponding to the HTTP status +code+ # # WEBrick::HTTPStatus.reason_phrase 404 # => "Not Found" def reason_phrase(code) StatusMessage[code.to_i] end ## # Is +code+ an informational status? def info?(code) code.to_i >= 100 and code.to_i < 200 end ## # Is +code+ a successful status? def success?(code) code.to_i >= 200 and code.to_i < 300 end ## # Is +code+ a redirection status? def redirect?(code) code.to_i >= 300 and code.to_i < 400 end ## # Is +code+ an error status? def error?(code) code.to_i >= 400 and code.to_i < 600 end ## # Is +code+ a client error status? def client_error?(code) code.to_i >= 400 and code.to_i < 500 end ## # Is +code+ a server error status? def server_error?(code) code.to_i >= 500 and code.to_i < 600 end ## # Returns the status class corresponding to +code+ # # WEBrick::HTTPStatus[302] # => WEBrick::HTTPStatus::NotFound # def self.[](code) CodeToError[code] end module_function :reason_phrase module_function :info?, :success?, :redirect?, :error? module_function :client_error?, :server_error? end end webrick-1.7.0/lib/webrick/httputils.rb000066400000000000000000000316631376456024300177510ustar00rootroot00000000000000# frozen_string_literal: false # # httputils.rb -- HTTPUtils Module # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httputils.rb,v 1.34 2003/06/05 21:34:08 gotoyuzo Exp $ require 'socket' require 'tempfile' module WEBrick CR = "\x0d" # :nodoc: LF = "\x0a" # :nodoc: CRLF = "\x0d\x0a" # :nodoc: ## # HTTPUtils provides utility methods for working with the HTTP protocol. # # This module is generally used internally by WEBrick module HTTPUtils ## # Normalizes a request path. Raises an exception if the path cannot be # normalized. def normalize_path(path) raise "abnormal path `#{path}'" if path[0] != ?/ ret = path.dup ret.gsub!(%r{/+}o, '/') # // => / while ret.sub!(%r'/\.(?:/|\Z)', '/'); end # /. => / while ret.sub!(%r'/(?!\.\./)[^/]+/\.\.(?:/|\Z)', '/'); end # /foo/.. => /foo raise "abnormal path `#{path}'" if %r{/\.\.(/|\Z)} =~ ret ret end module_function :normalize_path ## # Default mime types DefaultMimeTypes = { "ai" => "application/postscript", "asc" => "text/plain", "avi" => "video/x-msvideo", "bin" => "application/octet-stream", "bmp" => "image/bmp", "class" => "application/octet-stream", "cer" => "application/pkix-cert", "crl" => "application/pkix-crl", "crt" => "application/x-x509-ca-cert", #"crl" => "application/x-pkcs7-crl", "css" => "text/css", "dms" => "application/octet-stream", "doc" => "application/msword", "dvi" => "application/x-dvi", "eps" => "application/postscript", "etx" => "text/x-setext", "exe" => "application/octet-stream", "gif" => "image/gif", "htm" => "text/html", "html" => "text/html", "jpe" => "image/jpeg", "jpeg" => "image/jpeg", "jpg" => "image/jpeg", "js" => "application/javascript", "json" => "application/json", "lha" => "application/octet-stream", "lzh" => "application/octet-stream", "mjs" => "application/javascript", "mov" => "video/quicktime", "mpe" => "video/mpeg", "mpeg" => "video/mpeg", "mpg" => "video/mpeg", "pbm" => "image/x-portable-bitmap", "pdf" => "application/pdf", "pgm" => "image/x-portable-graymap", "png" => "image/png", "pnm" => "image/x-portable-anymap", "ppm" => "image/x-portable-pixmap", "ppt" => "application/vnd.ms-powerpoint", "ps" => "application/postscript", "qt" => "video/quicktime", "ras" => "image/x-cmu-raster", "rb" => "text/plain", "rd" => "text/plain", "rtf" => "application/rtf", "sgm" => "text/sgml", "sgml" => "text/sgml", "svg" => "image/svg+xml", "tif" => "image/tiff", "tiff" => "image/tiff", "txt" => "text/plain", "wasm" => "application/wasm", "xbm" => "image/x-xbitmap", "xhtml" => "text/html", "xls" => "application/vnd.ms-excel", "xml" => "text/xml", "xpm" => "image/x-xpixmap", "xwd" => "image/x-xwindowdump", "zip" => "application/zip", } ## # Loads Apache-compatible mime.types in +file+. def load_mime_types(file) # note: +file+ may be a "| command" for now; some people may # rely on this, but currently we do not use this method by default. open(file){ |io| hash = Hash.new io.each{ |line| next if /^#/ =~ line line.chomp! mimetype, ext0 = line.split(/\s+/, 2) next unless ext0 next if ext0.empty? ext0.split(/\s+/).each{ |ext| hash[ext] = mimetype } } hash } end module_function :load_mime_types ## # Returns the mime type of +filename+ from the list in +mime_tab+. If no # mime type was found application/octet-stream is returned. def mime_type(filename, mime_tab) suffix1 = (/\.(\w+)$/ =~ filename && $1.downcase) suffix2 = (/\.(\w+)\.[\w\-]+$/ =~ filename && $1.downcase) mime_tab[suffix1] || mime_tab[suffix2] || "application/octet-stream" end module_function :mime_type ## # Parses an HTTP header +raw+ into a hash of header fields with an Array # of values. def parse_header(raw) header = Hash.new([].freeze) field = nil raw.each_line{|line| case line when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om field, value = $1, $2 field.downcase! header[field] = [] unless header.has_key?(field) header[field] << value when /^\s+(.*?)\s*\z/om value = $1 unless field raise HTTPStatus::BadRequest, "bad header '#{line}'." end header[field][-1] << " " << value else raise HTTPStatus::BadRequest, "bad header '#{line}'." end } header.each{|key, values| values.each(&:strip!) } header end module_function :parse_header ## # Splits a header value +str+ according to HTTP specification. def split_header_value(str) str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+) (?:,\s*|\Z)'xn).flatten end module_function :split_header_value ## # Parses a Range header value +ranges_specifier+ def parse_range_header(ranges_specifier) if /^bytes=(.*)/ =~ ranges_specifier byte_range_set = split_header_value($1) byte_range_set.collect{|range_spec| case range_spec when /^(\d+)-(\d+)/ then $1.to_i .. $2.to_i when /^(\d+)-/ then $1.to_i .. -1 when /^-(\d+)/ then -($1.to_i) .. -1 else return nil end } end end module_function :parse_range_header ## # Parses q values in +value+ as used in Accept headers. def parse_qvalues(value) tmp = [] if value parts = value.split(/,\s*/) parts.each {|part| if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) val = m[1] q = (m[2] or 1).to_f tmp.push([val, q]) end } tmp = tmp.sort_by{|val, q| -q} tmp.collect!{|val, q| val} end return tmp end module_function :parse_qvalues ## # Removes quotes and escapes from +str+ def dequote(str) ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup ret.gsub!(/\\(.)/, "\\1") ret end module_function :dequote ## # Quotes and escapes quotes in +str+ def quote(str) '"' << str.gsub(/[\\\"]/o, "\\\1") << '"' end module_function :quote ## # Stores multipart form data. FormData objects are created when # WEBrick::HTTPUtils.parse_form_data is called. class FormData < String EmptyRawHeader = [].freeze # :nodoc: EmptyHeader = {}.freeze # :nodoc: ## # The name of the form data part attr_accessor :name ## # The filename of the form data part attr_accessor :filename attr_accessor :next_data # :nodoc: protected :next_data ## # Creates a new FormData object. # # +args+ is an Array of form data entries. One FormData will be created # for each entry. # # This is called by WEBrick::HTTPUtils.parse_form_data for you def initialize(*args) @name = @filename = @next_data = nil if args.empty? @raw_header = [] @header = nil super("") else @raw_header = EmptyRawHeader @header = EmptyHeader super(args.shift) unless args.empty? @next_data = self.class.new(*args) end end end ## # Retrieves the header at the first entry in +key+ def [](*key) begin @header[key[0].downcase].join(", ") rescue StandardError, NameError super end end ## # Adds +str+ to this FormData which may be the body, a header or a # header entry. # # This is called by WEBrick::HTTPUtils.parse_form_data for you def <<(str) if @header super elsif str == CRLF @header = HTTPUtils::parse_header(@raw_header.join) if cd = self['content-disposition'] if /\s+name="(.*?)"/ =~ cd then @name = $1 end if /\s+filename="(.*?)"/ =~ cd then @filename = $1 end end else @raw_header << str end self end ## # Adds +data+ at the end of the chain of entries # # This is called by WEBrick::HTTPUtils.parse_form_data for you. def append_data(data) tmp = self while tmp unless tmp.next_data tmp.next_data = data break end tmp = tmp.next_data end self end ## # Yields each entry in this FormData def each_data tmp = self while tmp next_data = tmp.next_data yield(tmp) tmp = next_data end end ## # Returns all the FormData as an Array def list ret = [] each_data{|data| ret << data.to_s } ret end ## # A FormData will behave like an Array alias :to_ary :list ## # This FormData's body def to_s String.new(self) end end ## # Parses the query component of a URI in +str+ def parse_query(str) query = Hash.new if str str.split(/[&;]/).each{|x| next if x.empty? key, val = x.split(/=/,2) key = unescape_form(key) val = unescape_form(val.to_s) val = FormData.new(val) val.name = key if query.has_key?(key) query[key].append_data(val) next end query[key] = val } end query end module_function :parse_query ## # Parses form data in +io+ with the given +boundary+ def parse_form_data(io, boundary) boundary_regexp = /\A--#{Regexp.quote(boundary)}(--)?#{CRLF}\z/ form_data = Hash.new return form_data unless io data = nil io.each_line{|line| if boundary_regexp =~ line if data data.chop! key = data.name if form_data.has_key?(key) form_data[key].append_data(data) else form_data[key] = data end end data = FormData.new next else if data data << line end end } return form_data end module_function :parse_form_data ##### reserved = ';/?:@&=+$,' num = '0123456789' lowalpha = 'abcdefghijklmnopqrstuvwxyz' upalpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' mark = '-_.!~*\'()' unreserved = num + lowalpha + upalpha + mark control = (0x0..0x1f).collect{|c| c.chr }.join + "\x7f" space = " " delims = '<>#%"' unwise = '{}|\\^[]`' nonascii = (0x80..0xff).collect{|c| c.chr }.join module_function # :stopdoc: def _make_regex(str) /([#{Regexp.escape(str)}])/n end def _make_regex!(str) /([^#{Regexp.escape(str)}])/n end def _escape(str, regex) str = str.b str.gsub!(regex) {"%%%02X" % $1.ord} # %-escaped string should contain US-ASCII only str.force_encoding(Encoding::US_ASCII) end def _unescape(str, regex) str = str.b str.gsub!(regex) {$1.hex.chr} # encoding of %-unescaped string is unknown str end UNESCAPED = _make_regex(control+space+delims+unwise+nonascii) UNESCAPED_FORM = _make_regex(reserved+control+delims+unwise+nonascii) NONASCII = _make_regex(nonascii) ESCAPED = /%([0-9a-fA-F]{2})/ UNESCAPED_PCHAR = _make_regex!(unreserved+":@&=+$,") # :startdoc: ## # Escapes HTTP reserved and unwise characters in +str+ def escape(str) _escape(str, UNESCAPED) end ## # Unescapes HTTP reserved and unwise characters in +str+ def unescape(str) _unescape(str, ESCAPED) end ## # Escapes form reserved characters in +str+ def escape_form(str) ret = _escape(str, UNESCAPED_FORM) ret.gsub!(/ /, "+") ret end ## # Unescapes form reserved characters in +str+ def unescape_form(str) _unescape(str.gsub(/\+/, " "), ESCAPED) end ## # Escapes path +str+ def escape_path(str) result = "" str.scan(%r{/([^/]*)}).each{|i| result << "/" << _escape(i[0], UNESCAPED_PCHAR) } return result end ## # Escapes 8 bit characters in +str+ def escape8bit(str) _escape(str, NONASCII) end end end webrick-1.7.0/lib/webrick/httpversion.rb000066400000000000000000000031521376456024300202660ustar00rootroot00000000000000# frozen_string_literal: false #-- # HTTPVersion.rb -- presentation of HTTP version # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: httpversion.rb,v 1.5 2002/09/21 12:23:37 gotoyuzo Exp $ module WEBrick ## # Represents an HTTP protocol version class HTTPVersion include Comparable ## # The major protocol version number attr_accessor :major ## # The minor protocol version number attr_accessor :minor ## # Converts +version+ into an HTTPVersion def self.convert(version) version.is_a?(self) ? version : new(version) end ## # Creates a new HTTPVersion from +version+. def initialize(version) case version when HTTPVersion @major, @minor = version.major, version.minor when String if /^(\d+)\.(\d+)$/ =~ version @major, @minor = $1.to_i, $2.to_i end end if @major.nil? || @minor.nil? raise ArgumentError, format("cannot convert %s into %s", version.class, self.class) end end ## # Compares this version with +other+ according to the HTTP specification # rules. def <=>(other) unless other.is_a?(self.class) other = self.class.new(other) end if (ret = @major <=> other.major) == 0 return @minor <=> other.minor end return ret end ## # The HTTP version as show in the HTTP request and response. For example, # "1.1" def to_s format("%d.%d", @major, @minor) end end end webrick-1.7.0/lib/webrick/log.rb000066400000000000000000000077701376456024300164740ustar00rootroot00000000000000# frozen_string_literal: false #-- # log.rb -- Log Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: log.rb,v 1.26 2002/10/06 17:06:10 gotoyuzo Exp $ module WEBrick ## # A generic logging class class BasicLog # Fatal log level which indicates a server crash FATAL = 1 # Error log level which indicates a recoverable error ERROR = 2 # Warning log level which indicates a possible problem WARN = 3 # Information log level which indicates possibly useful information INFO = 4 # Debugging error level for messages used in server development or # debugging DEBUG = 5 # log-level, messages above this level will be logged attr_accessor :level ## # Initializes a new logger for +log_file+ that outputs messages at +level+ # or higher. +log_file+ can be a filename, an IO-like object that # responds to #<< or nil which outputs to $stderr. # # If no level is given INFO is chosen by default def initialize(log_file=nil, level=nil) @level = level || INFO case log_file when String @log = File.open(log_file, "a+") @log.sync = true @opened = true when NilClass @log = $stderr else @log = log_file # requires "<<". (see BasicLog#log) end end ## # Closes the logger (also closes the log device associated to the logger) def close @log.close if @opened @log = nil end ## # Logs +data+ at +level+ if the given level is above the current log # level. def log(level, data) if @log && level <= @level data += "\n" if /\n\Z/ !~ data @log << data end end ## # Synonym for log(INFO, obj.to_s) def <<(obj) log(INFO, obj.to_s) end # Shortcut for logging a FATAL message def fatal(msg) log(FATAL, "FATAL " << format(msg)); end # Shortcut for logging an ERROR message def error(msg) log(ERROR, "ERROR " << format(msg)); end # Shortcut for logging a WARN message def warn(msg) log(WARN, "WARN " << format(msg)); end # Shortcut for logging an INFO message def info(msg) log(INFO, "INFO " << format(msg)); end # Shortcut for logging a DEBUG message def debug(msg) log(DEBUG, "DEBUG " << format(msg)); end # Will the logger output FATAL messages? def fatal?; @level >= FATAL; end # Will the logger output ERROR messages? def error?; @level >= ERROR; end # Will the logger output WARN messages? def warn?; @level >= WARN; end # Will the logger output INFO messages? def info?; @level >= INFO; end # Will the logger output DEBUG messages? def debug?; @level >= DEBUG; end private ## # Formats +arg+ for the logger # # * If +arg+ is an Exception, it will format the error message and # the back trace. # * If +arg+ responds to #to_str, it will return it. # * Otherwise it will return +arg+.inspect. def format(arg) if arg.is_a?(Exception) "#{arg.class}: #{AccessLog.escape(arg.message)}\n\t" << arg.backtrace.join("\n\t") << "\n" elsif arg.respond_to?(:to_str) AccessLog.escape(arg.to_str) else arg.inspect end end end ## # A logging class that prepends a timestamp to each message. class Log < BasicLog # Format of the timestamp which is applied to each logged line. The # default is "[%Y-%m-%d %H:%M:%S]" attr_accessor :time_format ## # Same as BasicLog#initialize # # You can set the timestamp format through #time_format def initialize(log_file=nil, level=nil) super(log_file, level) @time_format = "[%Y-%m-%d %H:%M:%S]" end ## # Same as BasicLog#log def log(level, data) tmp = Time.now.strftime(@time_format) tmp << " " << data super(level, tmp) end end end webrick-1.7.0/lib/webrick/server.rb000066400000000000000000000243111376456024300172070ustar00rootroot00000000000000# frozen_string_literal: false # # server.rb -- GenericServer Class # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: server.rb,v 1.62 2003/07/22 19:20:43 gotoyuzo Exp $ require 'socket' require_relative 'config' require_relative 'log' module WEBrick ## # Server error exception class ServerError < StandardError; end ## # Base server class class SimpleServer ## # A SimpleServer only yields when you start it def SimpleServer.start yield end end ## # A generic module for daemonizing a process class Daemon ## # Performs the standard operations for daemonizing a process. Runs a # block, if given. def Daemon.start Process.daemon File.umask(0) yield if block_given? end end ## # Base TCP server class. You must subclass GenericServer and provide a #run # method. class GenericServer ## # The server status. One of :Stop, :Running or :Shutdown attr_reader :status ## # The server configuration attr_reader :config ## # The server logger. This is independent from the HTTP access log. attr_reader :logger ## # Tokens control the number of outstanding clients. The # :MaxClients configuration sets this. attr_reader :tokens ## # Sockets listening for connections. attr_reader :listeners ## # Creates a new generic server from +config+. The default configuration # comes from +default+. def initialize(config={}, default=Config::General) @config = default.dup.update(config) @status = :Stop @config[:Logger] ||= Log::new @logger = @config[:Logger] @tokens = Thread::SizedQueue.new(@config[:MaxClients]) @config[:MaxClients].times{ @tokens.push(nil) } webrickv = WEBrick::VERSION rubyv = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" @logger.info("WEBrick #{webrickv}") @logger.info("ruby #{rubyv}") @listeners = [] @shutdown_pipe = nil unless @config[:DoNotListen] raise ArgumentError, "Port must an integer" unless @config[:Port].to_s == @config[:Port].to_i.to_s @config[:Port] = @config[:Port].to_i if @config[:Listen] warn(":Listen option is deprecated; use GenericServer#listen", uplevel: 1) end listen(@config[:BindAddress], @config[:Port]) if @config[:Port] == 0 @config[:Port] = @listeners[0].addr[1] end end end ## # Retrieves +key+ from the configuration def [](key) @config[key] end ## # Adds listeners from +address+ and +port+ to the server. See # WEBrick::Utils::create_listeners for details. def listen(address, port) @listeners += Utils::create_listeners(address, port) end ## # Starts the server and runs the +block+ for each connection. This method # does not return until the server is stopped from a signal handler or # another thread using #stop or #shutdown. # # If the block raises a subclass of StandardError the exception is logged # and ignored. If an IOError or Errno::EBADF exception is raised the # exception is ignored. If an Exception subclass is raised the exception # is logged and re-raised which stops the server. # # To completely shut down a server call #shutdown from ensure: # # server = WEBrick::GenericServer.new # # or WEBrick::HTTPServer.new # # begin # server.start # ensure # server.shutdown # end def start(&block) raise ServerError, "already started." if @status != :Stop server_type = @config[:ServerType] || SimpleServer setup_shutdown_pipe server_type.start{ @logger.info \ "#{self.class}#start: pid=#{$$} port=#{@config[:Port]}" @status = :Running call_callback(:StartCallback) shutdown_pipe = @shutdown_pipe thgroup = ThreadGroup.new begin while @status == :Running begin sp = shutdown_pipe[0] if svrs = IO.select([sp, *@listeners]) if svrs[0].include? sp # swallow shutdown pipe buf = String.new nil while String === sp.read_nonblock([sp.nread, 8].max, buf, exception: false) break end svrs[0].each{|svr| @tokens.pop # blocks while no token is there. if sock = accept_client(svr) unless config[:DoNotReverseLookup].nil? sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup] end th = start_thread(sock, &block) th[:WEBrickThread] = true thgroup.add(th) else @tokens.push(nil) end } end rescue Errno::EBADF, Errno::ENOTSOCK, IOError => ex # if the listening socket was closed in GenericServer#shutdown, # IO::select raise it. rescue StandardError => ex msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" @logger.error msg rescue Exception => ex @logger.fatal ex raise end end ensure cleanup_shutdown_pipe(shutdown_pipe) cleanup_listener @status = :Shutdown @logger.info "going to shutdown ..." thgroup.list.each{|th| th.join if th[:WEBrickThread] } call_callback(:StopCallback) @logger.info "#{self.class}#start done." @status = :Stop end } end ## # Stops the server from accepting new connections. def stop if @status == :Running @status = :Shutdown end alarm_shutdown_pipe {|f| f.write_nonblock("\0")} end ## # Shuts down the server and all listening sockets. New listeners must be # provided to restart the server. def shutdown stop alarm_shutdown_pipe(&:close) end ## # You must subclass GenericServer and implement \#run which accepts a TCP # client socket def run(sock) @logger.fatal "run() must be provided by user." end private # :stopdoc: ## # Accepts a TCP client socket from the TCP server socket +svr+ and returns # the client socket. def accept_client(svr) case sock = svr.to_io.accept_nonblock(exception: false) when :wait_readable nil else if svr.respond_to?(:start_immediately) sock = OpenSSL::SSL::SSLSocket.new(sock, ssl_context) sock.sync_close = true # we cannot do OpenSSL::SSL::SSLSocket#accept here because # a slow client can prevent us from accepting connections # from other clients end sock end rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINVAL nil rescue StandardError => ex msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" @logger.error msg nil end ## # Starts a server thread for the client socket +sock+ that runs the given # +block+. # # Sets the socket to the :WEBrickSocket thread local variable # in the thread. # # If any errors occur in the block they are logged and handled. def start_thread(sock, &block) Thread.start{ begin Thread.current[:WEBrickSocket] = sock begin addr = sock.peeraddr @logger.debug "accept: #{addr[3]}:#{addr[1]}" rescue SocketError @logger.debug "accept:
" raise end if sock.respond_to?(:sync_close=) && @config[:SSLStartImmediately] WEBrick::Utils.timeout(@config[:RequestTimeout]) do begin sock.accept # OpenSSL::SSL::SSLSocket#accept rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINVAL Thread.exit end end end call_callback(:AcceptCallback, sock) block ? block.call(sock) : run(sock) rescue Errno::ENOTCONN @logger.debug "Errno::ENOTCONN raised" rescue ServerError => ex msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}" @logger.error msg rescue Exception => ex @logger.error ex ensure @tokens.push(nil) Thread.current[:WEBrickSocket] = nil if addr @logger.debug "close: #{addr[3]}:#{addr[1]}" else @logger.debug "close:
" end sock.close end } end ## # Calls the callback +callback_name+ from the configuration with +args+ def call_callback(callback_name, *args) @config[callback_name]&.call(*args) end def setup_shutdown_pipe return @shutdown_pipe ||= IO.pipe end def cleanup_shutdown_pipe(shutdown_pipe) @shutdown_pipe = nil shutdown_pipe&.each(&:close) end def alarm_shutdown_pipe _, pipe = @shutdown_pipe # another thread may modify @shutdown_pipe. if pipe if !pipe.closed? begin yield pipe rescue IOError # closed by another thread. end end end end def cleanup_listener @listeners.each{|s| if @logger.debug? addr = s.addr @logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})") end begin s.shutdown rescue Errno::ENOTCONN # when `Errno::ENOTCONN: Socket is not connected' on some platforms, # call #close instead of #shutdown. # (ignore @config[:ShutdownSocketWithoutClose]) s.close else unless @config[:ShutdownSocketWithoutClose] s.close end end } @listeners.clear end end # end of GenericServer end webrick-1.7.0/lib/webrick/ssl.rb000066400000000000000000000164171376456024300165120ustar00rootroot00000000000000# frozen_string_literal: false # # ssl.rb -- SSL/TLS enhancement for GenericServer # # Copyright (c) 2003 GOTOU Yuuzou All rights reserved. # # $Id$ require 'webrick' require 'openssl' module WEBrick module Config svrsoft = General[:ServerSoftware] osslv = ::OpenSSL::OPENSSL_VERSION.split[1] ## # Default SSL server configuration. # # WEBrick can automatically create a self-signed certificate if # :SSLCertName is set. For more information on the various # SSL options see OpenSSL::SSL::SSLContext. # # :ServerSoftware :: # The server software name used in the Server: header. # :SSLEnable :: false, # Enable SSL for this server. Defaults to false. # :SSLCertificate :: # The SSL certificate for the server. # :SSLPrivateKey :: # The SSL private key for the server certificate. # :SSLClientCA :: nil, # Array of certificates that will be sent to the client. # :SSLExtraChainCert :: nil, # Array of certificates that will be added to the certificate chain # :SSLCACertificateFile :: nil, # Path to a CA certificate file # :SSLCACertificatePath :: nil, # Path to a directory containing CA certificates # :SSLCertificateStore :: nil, # OpenSSL::X509::Store used for certificate validation of the client # :SSLTmpDhCallback :: nil, # Callback invoked when DH parameters are required. # :SSLVerifyClient :: # Sets whether the client is verified. This defaults to VERIFY_NONE # which is typical for an HTTPS server. # :SSLVerifyDepth :: # Number of CA certificates to walk when verifying a certificate chain # :SSLVerifyCallback :: # Custom certificate verification callback # :SSLServerNameCallback:: # Custom servername indication callback # :SSLTimeout :: # Maximum session lifetime # :SSLOptions :: # Various SSL options # :SSLCiphers :: # Ciphers to be used # :SSLStartImmediately :: # Immediately start SSL upon connection? Defaults to true # :SSLCertName :: # SSL certificate name. Must be set to enable automatic certificate # creation. # :SSLCertComment :: # Comment used during automatic certificate creation. SSL = { :ServerSoftware => "#{svrsoft} OpenSSL/#{osslv}", :SSLEnable => false, :SSLCertificate => nil, :SSLPrivateKey => nil, :SSLClientCA => nil, :SSLExtraChainCert => nil, :SSLCACertificateFile => nil, :SSLCACertificatePath => nil, :SSLCertificateStore => nil, :SSLTmpDhCallback => nil, :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE, :SSLVerifyDepth => nil, :SSLVerifyCallback => nil, # custom verification :SSLTimeout => nil, :SSLOptions => nil, :SSLCiphers => nil, :SSLStartImmediately => true, # Must specify if you use auto generated certificate. :SSLCertName => nil, :SSLCertComment => "Generated by Ruby/OpenSSL" } General.update(SSL) end module Utils ## # Creates a self-signed certificate with the given number of +bits+, # the issuer +cn+ and a +comment+ to be stored in the certificate. def create_self_signed_cert(bits, cn, comment) rsa = OpenSSL::PKey::RSA.new(bits){|p, n| case p when 0; $stderr.putc "." # BN_generate_prime when 1; $stderr.putc "+" # BN_generate_prime when 2; $stderr.putc "*" # searching good prime, # n = #of try, # but also data from BN_generate_prime when 3; $stderr.putc "\n" # found good prime, n==0 - p, n==1 - q, # but also data from BN_generate_prime else; $stderr.putc "*" # BN_generate_prime end } cert = OpenSSL::X509::Certificate.new cert.version = 2 cert.serial = 1 name = (cn.kind_of? String) ? OpenSSL::X509::Name.parse(cn) : OpenSSL::X509::Name.new(cn) cert.subject = name cert.issuer = name cert.not_before = Time.now cert.not_after = Time.now + (365*24*60*60) cert.public_key = rsa.public_key ef = OpenSSL::X509::ExtensionFactory.new(nil,cert) ef.issuer_certificate = cert cert.extensions = [ ef.create_extension("basicConstraints","CA:FALSE"), ef.create_extension("keyUsage", "keyEncipherment, digitalSignature, keyAgreement, dataEncipherment"), ef.create_extension("subjectKeyIdentifier", "hash"), ef.create_extension("extendedKeyUsage", "serverAuth"), ef.create_extension("nsComment", comment), ] aki = ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always") cert.add_extension(aki) cert.sign(rsa, "SHA256") return [ cert, rsa ] end module_function :create_self_signed_cert end ## #-- # Updates WEBrick::GenericServer with SSL functionality class GenericServer ## # SSL context for the server when run in SSL mode def ssl_context # :nodoc: @ssl_context ||= begin if @config[:SSLEnable] ssl_context = setup_ssl_context(@config) @logger.info("\n" + @config[:SSLCertificate].to_text) ssl_context end end end undef listen ## # Updates +listen+ to enable SSL when the SSL configuration is active. def listen(address, port) # :nodoc: listeners = Utils::create_listeners(address, port) if @config[:SSLEnable] listeners.collect!{|svr| ssvr = ::OpenSSL::SSL::SSLServer.new(svr, ssl_context) ssvr.start_immediately = @config[:SSLStartImmediately] ssvr } end @listeners += listeners setup_shutdown_pipe end ## # Sets up an SSL context for +config+ def setup_ssl_context(config) # :nodoc: unless config[:SSLCertificate] cn = config[:SSLCertName] comment = config[:SSLCertComment] cert, key = Utils::create_self_signed_cert(2048, cn, comment) config[:SSLCertificate] = cert config[:SSLPrivateKey] = key end ctx = OpenSSL::SSL::SSLContext.new ctx.key = config[:SSLPrivateKey] ctx.cert = config[:SSLCertificate] ctx.client_ca = config[:SSLClientCA] ctx.extra_chain_cert = config[:SSLExtraChainCert] ctx.ca_file = config[:SSLCACertificateFile] ctx.ca_path = config[:SSLCACertificatePath] ctx.cert_store = config[:SSLCertificateStore] ctx.tmp_dh_callback = config[:SSLTmpDhCallback] ctx.verify_mode = config[:SSLVerifyClient] ctx.verify_depth = config[:SSLVerifyDepth] ctx.verify_callback = config[:SSLVerifyCallback] ctx.servername_cb = config[:SSLServerNameCallback] || proc { |args| ssl_servername_callback(*args) } ctx.timeout = config[:SSLTimeout] ctx.options = config[:SSLOptions] ctx.ciphers = config[:SSLCiphers] ctx end ## # ServerNameIndication callback def ssl_servername_callback(sslsocket, hostname = nil) # default end end end webrick-1.7.0/lib/webrick/utils.rb000066400000000000000000000155501376456024300170460ustar00rootroot00000000000000# frozen_string_literal: false # # utils.rb -- Miscellaneous utilities # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou # Copyright (c) 2002 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: utils.rb,v 1.10 2003/02/16 22:22:54 gotoyuzo Exp $ require 'socket' require 'io/nonblock' require 'etc' module WEBrick module Utils ## # Sets IO operations on +io+ to be non-blocking def set_non_blocking(io) io.nonblock = true if io.respond_to?(:nonblock=) end module_function :set_non_blocking ## # Sets the close on exec flag for +io+ def set_close_on_exec(io) io.close_on_exec = true if io.respond_to?(:close_on_exec=) end module_function :set_close_on_exec ## # Changes the process's uid and gid to the ones of +user+ def su(user) if pw = Etc.getpwnam(user) Process::initgroups(user, pw.gid) Process::Sys::setgid(pw.gid) Process::Sys::setuid(pw.uid) else warn("WEBrick::Utils::su doesn't work on this platform", uplevel: 1) end end module_function :su ## # The server hostname def getservername Socket::gethostname end module_function :getservername ## # Creates TCP server sockets bound to +address+:+port+ and returns them. # # It will create IPV4 and IPV6 sockets on all interfaces. def create_listeners(address, port) unless port raise ArgumentError, "must specify port" end sockets = Socket.tcp_server_sockets(address, port) sockets = sockets.map {|s| s.autoclose = false ts = TCPServer.for_fd(s.fileno) s.close ts } return sockets end module_function :create_listeners ## # Characters used to generate random strings RAND_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "abcdefghijklmnopqrstuvwxyz" ## # Generates a random string of length +len+ def random_string(len) rand_max = RAND_CHARS.bytesize ret = "" len.times{ ret << RAND_CHARS[rand(rand_max)] } ret end module_function :random_string ########### require "timeout" require "singleton" ## # Class used to manage timeout handlers across multiple threads. # # Timeout handlers should be managed by using the class methods which are # synchronized. # # id = TimeoutHandler.register(10, Timeout::Error) # begin # sleep 20 # puts 'foo' # ensure # TimeoutHandler.cancel(id) # end # # will raise Timeout::Error # # id = TimeoutHandler.register(10, Timeout::Error) # begin # sleep 5 # puts 'foo' # ensure # TimeoutHandler.cancel(id) # end # # will print 'foo' # class TimeoutHandler include Singleton ## # Mutex used to synchronize access across threads TimeoutMutex = Thread::Mutex.new # :nodoc: ## # Registers a new timeout handler # # +time+:: Timeout in seconds # +exception+:: Exception to raise when timeout elapsed def TimeoutHandler.register(seconds, exception) at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds instance.register(Thread.current, at, exception) end ## # Cancels the timeout handler +id+ def TimeoutHandler.cancel(id) instance.cancel(Thread.current, id) end def self.terminate instance.terminate end ## # Creates a new TimeoutHandler. You should use ::register and ::cancel # instead of creating the timeout handler directly. def initialize TimeoutMutex.synchronize{ @timeout_info = Hash.new } @queue = Thread::Queue.new @watcher = nil end # :nodoc: private \ def watch to_interrupt = [] while true now = Process.clock_gettime(Process::CLOCK_MONOTONIC) wakeup = nil to_interrupt.clear TimeoutMutex.synchronize{ @timeout_info.each {|thread, ary| next unless ary ary.each{|info| time, exception = *info if time < now to_interrupt.push [thread, info.object_id, exception] elsif !wakeup || time < wakeup wakeup = time end } } } to_interrupt.each {|arg| interrupt(*arg)} if !wakeup @queue.pop elsif (wakeup -= now) > 0 begin (th = Thread.start {@queue.pop}).join(wakeup) ensure th&.kill&.join end end @queue.clear end end # :nodoc: private \ def watcher (w = @watcher)&.alive? and return w # usual case TimeoutMutex.synchronize{ (w = @watcher)&.alive? and next w # pathological check @watcher = Thread.start(&method(:watch)) } end ## # Interrupts the timeout handler +id+ and raises +exception+ def interrupt(thread, id, exception) if cancel(thread, id) && thread.alive? thread.raise(exception, "execution timeout") end end ## # Registers a new timeout handler # # +time+:: Timeout in seconds # +exception+:: Exception to raise when timeout elapsed def register(thread, time, exception) info = nil TimeoutMutex.synchronize{ (@timeout_info[thread] ||= []) << (info = [time, exception]) } @queue.push nil watcher return info.object_id end ## # Cancels the timeout handler +id+ def cancel(thread, id) TimeoutMutex.synchronize{ if ary = @timeout_info[thread] ary.delete_if{|info| info.object_id == id } if ary.empty? @timeout_info.delete(thread) end return true end return false } end ## def terminate TimeoutMutex.synchronize{ @timeout_info.clear @watcher&.kill&.join } end end ## # Executes the passed block and raises +exception+ if execution takes more # than +seconds+. # # If +seconds+ is zero or nil, simply executes the block def timeout(seconds, exception=Timeout::Error) return yield if seconds.nil? or seconds.zero? # raise ThreadError, "timeout within critical session" if Thread.critical id = TimeoutHandler.register(seconds, exception) begin yield(seconds) ensure TimeoutHandler.cancel(id) end end module_function :timeout end end webrick-1.7.0/lib/webrick/version.rb000066400000000000000000000006371376456024300173730ustar00rootroot00000000000000# frozen_string_literal: false #-- # version.rb -- version and release date # # Author: IPR -- Internet Programming with Ruby -- writers # Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU # Copyright (c) 2003 Internet Programming with Ruby writers. All rights # reserved. # # $IPR: version.rb,v 1.74 2003/07/22 19:20:43 gotoyuzo Exp $ module WEBrick ## # The WEBrick version VERSION = "1.7.0" end webrick-1.7.0/test/000077500000000000000000000000001376456024300141365ustar00rootroot00000000000000webrick-1.7.0/test/lib/000077500000000000000000000000001376456024300147045ustar00rootroot00000000000000webrick-1.7.0/test/lib/envutil.rb000066400000000000000000000204401376456024300167170ustar00rootroot00000000000000# -*- coding: us-ascii -*- # frozen_string_literal: true require "open3" require "timeout" require_relative "find_executable" begin require 'rbconfig' rescue LoadError end begin require "rbconfig/sizeof" rescue LoadError end module EnvUtil def rubybin if ruby = ENV["RUBY"] return ruby end ruby = "ruby" exeext = RbConfig::CONFIG["EXEEXT"] rubyexe = (ruby + exeext if exeext and !exeext.empty?) 3.times do if File.exist? ruby and File.executable? ruby and !File.directory? ruby return File.expand_path(ruby) end if rubyexe and File.exist? rubyexe and File.executable? rubyexe return File.expand_path(rubyexe) end ruby = File.join("..", ruby) end if defined?(RbConfig.ruby) RbConfig.ruby else "ruby" end end module_function :rubybin LANG_ENVS = %w"LANG LC_ALL LC_CTYPE" DEFAULT_SIGNALS = Signal.list DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM class << self attr_accessor :subprocess_timeout_scale end def apply_timeout_scale(t) if scale = EnvUtil.subprocess_timeout_scale t * scale else t end end module_function :apply_timeout_scale def timeout(sec, klass = nil, message = nil, &blk) return yield(sec) if sec == nil or sec.zero? sec = apply_timeout_scale(sec) Timeout.timeout(sec, klass, message, &blk) end module_function :timeout def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false, encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error, stdout_filter: nil, stderr_filter: nil, signal: :TERM, rubybin: EnvUtil.rubybin, **opt) timeout = apply_timeout_scale(timeout) reprieve = apply_timeout_scale(reprieve) if reprieve in_c, in_p = IO.pipe out_p, out_c = IO.pipe if capture_stdout err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout opt[:in] = in_c opt[:out] = out_c if capture_stdout opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr if encoding out_p.set_encoding(encoding) if out_p err_p.set_encoding(encoding) if err_p end c = "C" child_env = {} LANG_ENVS.each {|lc| child_env[lc] = c} if Array === args and Hash === args.first child_env.update(args.shift) end args = [args] if args.kind_of?(String) pid = spawn(child_env, rubybin, *args, **opt) in_c.close out_c.close if capture_stdout err_c.close if capture_stderr && capture_stderr != :merge_to_stdout if block_given? return yield in_p, out_p, err_p, pid else th_stdout = Thread.new { out_p.read } if capture_stdout th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout in_p.write stdin_data.to_str unless stdin_data.empty? in_p.close if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) timeout_error = nil else signals = Array(signal).select do |sig| DEFAULT_SIGNALS[sig.to_s] or DEFAULT_SIGNALS[Signal.signame(sig)] rescue false end signals |= [:ABRT, :KILL] case pgroup = opt[:pgroup] when 0, true pgroup = -pid when nil, false pgroup = pid end while signal = signals.shift begin Process.kill signal, pgroup rescue Errno::EINVAL next rescue Errno::ESRCH break end if signals.empty? or !reprieve Process.wait(pid) else begin Timeout.timeout(reprieve) {Process.wait(pid)} rescue Timeout::Error end end end status = $? end stdout = th_stdout.value if capture_stdout stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout out_p.close if capture_stdout err_p.close if capture_stderr && capture_stderr != :merge_to_stdout status ||= Process.wait2(pid)[1] stdout = stdout_filter.call(stdout) if stdout_filter stderr = stderr_filter.call(stderr) if stderr_filter if timeout_error bt = caller_locations msg = "execution of #{bt.shift.label} expired" msg = Test::Unit::Assertions::FailDesc[status, msg, [stdout, stderr].join("\n")].() raise timeout_error, msg, bt.map(&:to_s) end return stdout, stderr, status end ensure [th_stdout, th_stderr].each do |th| th.kill if th end [in_c, in_p, out_c, out_p, err_c, err_p].each do |io| io&.close end [th_stdout, th_stderr].each do |th| th.join if th end end module_function :invoke_ruby alias rubyexec invoke_ruby class << self alias rubyexec invoke_ruby end def verbose_warning class << (stderr = "".dup) alias write << end stderr, $stderr, verbose, $VERBOSE = $stderr, stderr, $VERBOSE, true yield stderr return $stderr ensure stderr, $stderr, $VERBOSE = $stderr, stderr, verbose end module_function :verbose_warning def default_warning verbose, $VERBOSE = $VERBOSE, false yield ensure $VERBOSE = verbose end module_function :default_warning def suppress_warning verbose, $VERBOSE = $VERBOSE, nil yield ensure $VERBOSE = verbose end module_function :suppress_warning def under_gc_stress(stress = true) stress, GC.stress = GC.stress, stress yield ensure GC.stress = stress end module_function :under_gc_stress def with_default_external(enc) verbose, $VERBOSE = $VERBOSE, nil origenc, Encoding.default_external = Encoding.default_external, enc $VERBOSE = verbose yield ensure verbose, $VERBOSE = $VERBOSE, nil Encoding.default_external = origenc $VERBOSE = verbose end module_function :with_default_external def with_default_internal(enc) verbose, $VERBOSE = $VERBOSE, nil origenc, Encoding.default_internal = Encoding.default_internal, enc $VERBOSE = verbose yield ensure verbose, $VERBOSE = $VERBOSE, nil Encoding.default_internal = origenc $VERBOSE = verbose end module_function :with_default_internal def labeled_module(name, &block) Module.new do singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s} class_eval(&block) if block end end module_function :labeled_module def labeled_class(name, superclass = Object, &block) Class.new(superclass) do singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s} class_eval(&block) if block end end module_function :labeled_class if /darwin/ =~ RUBY_PLATFORM DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports") DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S' @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME'] def self.diagnostic_reports(signame, pid, now) return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame) cmd = File.basename(rubybin) cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd path = DIAGNOSTIC_REPORTS_PATH timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash" first = true 30.times do first ? (first = false) : sleep(0.1) Dir.glob(pat) do |name| log = File.read(name) rescue next if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log File.unlink(name) File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil return log end end end nil end else def self.diagnostic_reports(signame, pid, now) end end def self.gc_stress_to_class? unless defined?(@gc_stress_to_class) _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"]) @gc_stress_to_class = status.success? end @gc_stress_to_class end end if defined?(RbConfig) module RbConfig @ruby = EnvUtil.rubybin class << self undef ruby if method_defined?(:ruby) attr_reader :ruby end dir = File.dirname(ruby) CONFIG['bindir'] = dir Gem::ConfigMap[:bindir] = dir if defined?(Gem::ConfigMap) end end webrick-1.7.0/test/lib/find_executable.rb000066400000000000000000000011031376456024300203450ustar00rootroot00000000000000# frozen_string_literal: true require "rbconfig" module EnvUtil def find_executable(cmd, *args) exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]] ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| next if path.empty? path = File.join(path, cmd) exts.each do |ext| cmdline = [path + ext, *args] begin return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read)) rescue next end end end nil end module_function :find_executable end webrick-1.7.0/test/lib/leakchecker.rb000066400000000000000000000120061376456024300174710ustar00rootroot00000000000000# frozen_string_literal: true class LeakChecker def initialize @fd_info = find_fds @tempfile_info = find_tempfiles @thread_info = find_threads @env_info = find_env end def check(test_name) leaks = [ check_fd_leak(test_name), check_thread_leak(test_name), check_tempfile_leak(test_name), check_env(test_name) ] GC.start if leaks.any? end def find_fds if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero? m[:close] end fd_dir = "/proc/self/fd" if File.directory?(fd_dir) fds = Dir.open(fd_dir) {|d| a = d.grep(/\A\d+\z/, &:to_i) if d.respond_to? :fileno a -= [d.fileno] end a } fds.sort else [] end end def check_fd_leak(test_name) leaked = false live1 = @fd_info live2 = find_fds fd_closed = live1 - live2 if !fd_closed.empty? fd_closed.each {|fd| puts "Closed file descriptor: #{test_name}: #{fd}" } end fd_leaked = live2 - live1 if !fd_leaked.empty? leaked = true h = {} ObjectSpace.each_object(IO) {|io| inspect = io.inspect begin autoclose = io.autoclose? fd = io.fileno rescue IOError # closed IO object next end (h[fd] ||= []) << [io, autoclose, inspect] } fd_leaked.each {|fd| str = ''.dup if h[fd] str << ' :' h[fd].map {|io, autoclose, inspect| s = ' ' + inspect s << "(not-autoclose)" if !autoclose s }.sort.each {|s| str << s } end puts "Leaked file descriptor: #{test_name}: #{fd}#{str}" } #system("lsof -p #$$") if !fd_leaked.empty? h.each {|fd, list| next if list.length <= 1 if 1 < list.count {|io, autoclose, inspect| autoclose } str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join puts "Multiple autoclose IO object for a file descriptor:#{str}" end } end @fd_info = live2 return leaked end def extend_tempfile_counter return if defined? LeakChecker::TempfileCounter m = Module.new { @count = 0 class << self attr_accessor :count end def new(data) LeakChecker::TempfileCounter.count += 1 super(data) end } LeakChecker.const_set(:TempfileCounter, m) class << Tempfile::Remover prepend LeakChecker::TempfileCounter end end def find_tempfiles(prev_count=-1) return [prev_count, []] unless defined? Tempfile extend_tempfile_counter count = TempfileCounter.count if prev_count == count [prev_count, []] else tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.instance_variable_defined?(:@tmpfile) and t.path } [count, tempfiles] end end def check_tempfile_leak(test_name) return false unless defined? Tempfile count1, initial_tempfiles = @tempfile_info count2, current_tempfiles = find_tempfiles(count1) leaked = false tempfiles_leaked = current_tempfiles - initial_tempfiles if !tempfiles_leaked.empty? leaked = true list = tempfiles_leaked.map {|t| t.inspect }.sort list.each {|str| puts "Leaked tempfile: #{test_name}: #{str}" } tempfiles_leaked.each {|t| t.close! } end @tempfile_info = [count2, initial_tempfiles] return leaked end def find_threads Thread.list.find_all {|t| t != Thread.current && t.alive? } end def check_thread_leak(test_name) live1 = @thread_info live2 = find_threads thread_finished = live1 - live2 leaked = false if !thread_finished.empty? list = thread_finished.map {|t| t.inspect }.sort list.each {|str| puts "Finished thread: #{test_name}: #{str}" } end thread_leaked = live2 - live1 if !thread_leaked.empty? leaked = true list = thread_leaked.map {|t| t.inspect }.sort list.each {|str| puts "Leaked thread: #{test_name}: #{str}" } end @thread_info = live2 return leaked end def find_env ENV.to_h end def check_env(test_name) old_env = @env_info new_env = ENV.to_h return false if old_env == new_env (old_env.keys | new_env.keys).sort.each {|k| if old_env.has_key?(k) if new_env.has_key?(k) if old_env[k] != new_env[k] puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}" end else puts "Environment variable changed: #{test_name} : #{k.inspect} deleted" end else if new_env.has_key?(k) puts "Environment variable changed: #{test_name} : #{k.inspect} added" else flunk "unreachable" end end } @env_info = new_env return true end def puts(*a) MiniTest::Unit.output.puts(*a) end end webrick-1.7.0/test/lib/minitest/000077500000000000000000000000001376456024300165405ustar00rootroot00000000000000webrick-1.7.0/test/lib/minitest/autorun.rb000066400000000000000000000003171376456024300205630ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true begin require 'rubygems' gem 'minitest' rescue Gem::LoadError # do nothing end require 'minitest/unit' require 'minitest/mock' MiniTest::Unit.autorun webrick-1.7.0/test/lib/minitest/mock.rb000066400000000000000000000131621376456024300200210ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true class MockExpectationError < StandardError; end # :nodoc: ## # A simple and clean mock object framework. module MiniTest # :nodoc: ## # All mock objects are an instance of Mock class Mock alias :__respond_to? :respond_to? skip_methods = %w(object_id respond_to_missing? inspect === to_s) instance_methods.each do |m| undef_method m unless skip_methods.include?(m.to_s) || m =~ /^__/ end def initialize # :nodoc: @expected_calls = Hash.new { |calls, name| calls[name] = [] } @actual_calls = Hash.new { |calls, name| calls[name] = [] } end ## # Expect that method +name+ is called, optionally with +args+ or a # +blk+, and returns +retval+. # # @mock.expect(:meaning_of_life, 42) # @mock.meaning_of_life # => 42 # # @mock.expect(:do_something_with, true, [some_obj, true]) # @mock.do_something_with(some_obj, true) # => true # # @mock.expect(:do_something_else, true) do |a1, a2| # a1 == "buggs" && a2 == :bunny # end # # +args+ is compared to the expected args using case equality (ie, the # '===' operator), allowing for less specific expectations. # # @mock.expect(:uses_any_string, true, [String]) # @mock.uses_any_string("foo") # => true # @mock.verify # => true # # @mock.expect(:uses_one_string, true, ["foo"] # @mock.uses_one_string("bar") # => true # @mock.verify # => raises MockExpectationError def expect(name, retval, args=[], &blk) if block_given? raise ArgumentError, "args ignored when block given" unless args.empty? @expected_calls[name] << { :retval => retval, :block => blk } else raise ArgumentError, "args must be an array" unless Array === args @expected_calls[name] << { :retval => retval, :args => args } end self end def __call name, data # :nodoc: case data when Hash then "#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}" else data.map { |d| __call name, d }.join ", " end end ## # Verify that all methods were called as expected. Raises # +MockExpectationError+ if the mock object was not called as # expected. def verify @expected_calls.each do |name, calls| calls.each do |expected| msg1 = "expected #{__call name, expected}" msg2 = "#{msg1}, got [#{__call name, @actual_calls[name]}]" raise MockExpectationError, msg2 if @actual_calls.has_key?(name) and not @actual_calls[name].include?(expected) raise MockExpectationError, msg1 unless @actual_calls.has_key?(name) and @actual_calls[name].include?(expected) end end true end def method_missing(sym, *args) # :nodoc: unless @expected_calls.has_key?(sym) then raise NoMethodError, "unmocked method %p, expected one of %p" % [sym, @expected_calls.keys.sort_by(&:to_s)] end index = @actual_calls[sym].length expected_call = @expected_calls[sym][index] unless expected_call then raise MockExpectationError, "No more expects available for %p: %p" % [sym, args] end expected_args, retval, val_block = expected_call.values_at(:args, :retval, :block) if val_block then raise MockExpectationError, "mocked method %p failed block w/ %p" % [sym, args] unless val_block.call(args) # keep "verify" happy @actual_calls[sym] << expected_call return retval end if expected_args.size != args.size then raise ArgumentError, "mocked method %p expects %d arguments, got %d" % [sym, expected_args.size, args.size] end fully_matched = expected_args.zip(args).all? { |mod, a| mod === a or mod == a } unless fully_matched then raise MockExpectationError, "mocked method %p called with unexpected arguments %p" % [sym, args] end @actual_calls[sym] << { :retval => retval, :args => expected_args.zip(args).map { |mod, a| mod === a ? mod : a } } retval end def respond_to?(sym, include_private = false) # :nodoc: return true if @expected_calls.has_key?(sym.to_sym) return __respond_to?(sym, include_private) end end end class Object # :nodoc: ## # Add a temporary stubbed method replacing +name+ for the duration # of the +block+. If +val_or_callable+ responds to #call, then it # returns the result of calling it, otherwise returns the value # as-is. Cleans up the stub at the end of the +block+. The method # +name+ must exist before stubbing. # # def test_stale_eh # obj_under_test = Something.new # refute obj_under_test.stale? # # Time.stub :now, Time.at(0) do # assert obj_under_test.stale? # end # end def stub name, val_or_callable, &block new_name = "__minitest_stub__#{name}" metaclass = class << self; self; end if respond_to? name and not methods.map(&:to_s).include? name.to_s then metaclass.send :define_method, name do |*args| super(*args) end end metaclass.send :alias_method, new_name, name metaclass.send :define_method, name do |*args| if val_or_callable.respond_to? :call then val_or_callable.call(*args) else val_or_callable end end yield self ensure metaclass.send :undef_method, name metaclass.send :alias_method, name, new_name metaclass.send :undef_method, new_name end end webrick-1.7.0/test/lib/minitest/unit.rb000066400000000000000000001071361376456024300200540ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true require "optparse" require "rbconfig" require "leakchecker" ## # Minimal (mostly drop-in) replacement for test-unit. # # :include: README.txt module MiniTest def self.const_missing name # :nodoc: case name when :MINI_DIR then msg = "MiniTest::MINI_DIR was removed. Don't violate other's internals." warn "WAR\NING: #{msg}" warn "WAR\NING: Used by #{caller.first}." const_set :MINI_DIR, "bad value" else super end end ## # Assertion base class class Assertion < Exception; end ## # Assertion raised when skipping a test class Skip < Assertion; end class << self ## # Filter object for backtraces. attr_accessor :backtrace_filter end class BacktraceFilter # :nodoc: def filter bt return ["No backtrace"] unless bt new_bt = [] unless $DEBUG then bt.each do |line| break if line =~ /lib\/minitest/ new_bt << line end new_bt = bt.reject { |line| line =~ /lib\/minitest/ } if new_bt.empty? new_bt = bt.dup if new_bt.empty? else new_bt = bt.dup end new_bt end end self.backtrace_filter = BacktraceFilter.new def self.filter_backtrace bt # :nodoc: backtrace_filter.filter bt end ## # MiniTest Assertions. All assertion methods accept a +msg+ which is # printed if the assertion fails. module Assertions ## # Returns the diff command to use in #diff. Tries to intelligently # figure out what diff to use. def self.diff @diff = if (RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ && system("diff.exe", __FILE__, __FILE__)) then "diff.exe -u" elsif Minitest::Unit::Guard.maglev? then # HACK "diff -u" elsif system("gdiff", __FILE__, __FILE__) "gdiff -u" # solaris and kin suck elsif system("diff", __FILE__, __FILE__) "diff -u" else nil end unless defined? @diff @diff end ## # Set the diff command to use in #diff. def self.diff= o @diff = o end ## # Returns a diff between +exp+ and +act+. If there is no known # diff command or if it doesn't make sense to diff the output # (single line, short output), then it simply returns a basic # comparison between the two. def diff exp, act require "tempfile" expect = mu_pp_for_diff exp butwas = mu_pp_for_diff act result = nil need_to_diff = MiniTest::Assertions.diff && (expect.include?("\n") || butwas.include?("\n") || expect.size > 30 || butwas.size > 30 || expect == butwas) return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless need_to_diff tempfile_a = nil tempfile_b = nil Tempfile.open("expect") do |a| tempfile_a = a a.puts expect a.flush Tempfile.open("butwas") do |b| tempfile_b = b b.puts butwas b.flush result = `#{MiniTest::Assertions.diff} #{a.path} #{b.path}` result.sub!(/^\-\-\- .+/, "--- expected") result.sub!(/^\+\+\+ .+/, "+++ actual") if result.empty? then klass = exp.class result = [ "No visible difference in the #{klass}#inspect output.\n", "You should look at the implementation of #== on ", "#{klass} or its members.\n", expect, ].join end end end result ensure tempfile_a.close! if tempfile_a tempfile_b.close! if tempfile_b end ## # This returns a human-readable version of +obj+. By default # #inspect is called. You can override this to use #pretty_print # if you want. def mu_pp obj s = obj.inspect s = s.encode Encoding.default_external if defined? Encoding s end ## # This returns a diff-able human-readable version of +obj+. This # differs from the regular mu_pp because it expands escaped # newlines and makes hex-values generic (like object_ids). This # uses mu_pp to do the first pass and then cleans it up. def mu_pp_for_diff obj mu_pp(obj).gsub(/\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ':0xXXXXXX') end def _assertions= n # :nodoc: @_assertions = n end def _assertions # :nodoc: @_assertions ||= 0 end ## # Fails unless +test+ is a true value. def assert test, msg = nil msg ||= "Failed assertion, no message given." self._assertions += 1 unless test then msg = msg.call if Proc === msg raise MiniTest::Assertion, msg end true end ## # Fails unless +obj+ is empty. def assert_empty obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" } assert_respond_to obj, :empty? assert obj.empty?, msg end ## # Fails unless exp == act printing the difference between # the two, if possible. # # If there is no visible difference but the assertion fails, you # should suspect that your #== is buggy, or your inspect output is # missing crucial details. # # For floats use assert_in_delta. # # See also: MiniTest::Assertions.diff def assert_equal exp, act, msg = nil msg = message(msg, "") { diff exp, act } assert exp == act, msg end ## # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+ # of each other. # # assert_in_delta Math::PI, (22.0 / 7.0), 0.01 def assert_in_delta exp, act, delta = 0.001, msg = nil n = (exp - act).abs msg = message(msg) { "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}" } assert delta >= n, msg end ## # For comparing Floats. Fails unless +exp+ and +act+ have a relative # error less than +epsilon+. def assert_in_epsilon a, b, epsilon = 0.001, msg = nil assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg end ## # Fails unless +collection+ includes +obj+. def assert_includes collection, obj, msg = nil msg = message(msg) { "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}" } assert_respond_to collection, :include? assert collection.include?(obj), msg end ## # Fails unless +obj+ is an instance of +cls+. def assert_instance_of cls, obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}" } assert obj.instance_of?(cls), msg end ## # Fails unless +obj+ is a kind of +cls+. def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of msg = message(msg) { "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" } assert obj.kind_of?(cls), msg end ## # Fails unless +matcher+ =~ +obj+. def assert_match matcher, obj, msg = nil msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" } assert_respond_to matcher, :"=~" matcher = Regexp.new Regexp.escape matcher if String === matcher assert matcher =~ obj, msg end ## # Fails unless +obj+ is nil def assert_nil obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" } assert obj.nil?, msg end ## # For testing with binary operators. # # assert_operator 5, :<=, 4 def assert_operator o1, op, o2 = (predicate = true; nil), msg = nil return assert_predicate o1, op, msg if predicate msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" } assert o1.__send__(op, o2), msg end ## # Fails if stdout or stderr do not output the expected results. # Pass in nil if you don't care about that streams output. Pass in # "" if you require it to be silent. Pass in a regexp if you want # to pattern match. # # NOTE: this uses #capture_io, not #capture_subprocess_io. # # See also: #assert_silent def assert_output stdout = nil, stderr = nil out, err = capture_io do yield end err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout y = send err_msg, stderr, err, "In stderr" if err_msg x = send out_msg, stdout, out, "In stdout" if out_msg (!stdout || x) && (!stderr || y) end ## # For testing with predicates. # # assert_predicate str, :empty? # # This is really meant for specs and is front-ended by assert_operator: # # str.must_be :empty? def assert_predicate o1, op, msg = nil msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" } assert o1.__send__(op), msg end ## # Fails unless the block raises one of +exp+. Returns the # exception matched so you can check the message, attributes, etc. def assert_raises *exp msg = "#{exp.pop}.\n" if String === exp.last begin yield rescue MiniTest::Skip => e return e if exp.include? MiniTest::Skip raise e rescue Exception => e expected = exp.any? { |ex| if ex.instance_of? Module then e.kind_of? ex else e.instance_of? ex end } assert expected, proc { exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not") } return e end exp = exp.first if exp.size == 1 flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised." end ## # Fails unless +obj+ responds to +meth+. def assert_respond_to obj, meth, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}" } assert obj.respond_to?(meth), msg end ## # Fails unless +exp+ and +act+ are #equal? def assert_same exp, act, msg = nil msg = message(msg) { data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data } assert exp.equal?(act), msg end ## # +send_ary+ is a receiver, message and arguments. # # Fails unless the call returns a true value # TODO: I should prolly remove this from specs def assert_send send_ary, m = nil recv, msg, *args = send_ary m = message(m) { "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" } assert recv.__send__(msg, *args), m end ## # Fails if the block outputs anything to stderr or stdout. # # See also: #assert_output def assert_silent assert_output "", "" do yield end end ## # Fails unless the block throws +sym+ def assert_throws sym, msg = nil default = "Expected #{mu_pp(sym)} to have been thrown" caught = true catch(sym) do begin yield rescue ThreadError => e # wtf?!? 1.8 + threads == suck default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}" rescue ArgumentError => e # 1.9 exception default += ", not #{e.message.split(/ /).last}" rescue NameError => e # 1.8 exception default += ", not #{e.name.inspect}" end caught = false end assert caught, message(msg) { default } end ## # Captures $stdout and $stderr into strings: # # out, err = capture_io do # puts "Some info" # warn "You did a bad thing" # end # # assert_match %r%info%, out # assert_match %r%bad%, err # # NOTE: For efficiency, this method uses StringIO and does not # capture IO for subprocesses. Use #capture_subprocess_io for # that. def capture_io require 'stringio' captured_stdout, captured_stderr = StringIO.new, StringIO.new synchronize do orig_stdout, orig_stderr = $stdout, $stderr $stdout, $stderr = captured_stdout, captured_stderr begin yield ensure $stdout = orig_stdout $stderr = orig_stderr end end return captured_stdout.string, captured_stderr.string end alias capture_output capture_io ## # Captures $stdout and $stderr into strings, using Tempfile to # ensure that subprocess IO is captured as well. # # out, err = capture_subprocess_io do # system "echo Some info" # system "echo You did a bad thing 1>&2" # end # # assert_match %r%info%, out # assert_match %r%bad%, err # # NOTE: This method is approximately 10x slower than #capture_io so # only use it when you need to test the output of a subprocess. def capture_subprocess_io require 'tempfile' captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err") synchronize do orig_stdout, orig_stderr = $stdout.dup, $stderr.dup $stdout.reopen captured_stdout $stderr.reopen captured_stderr begin yield $stdout.rewind $stderr.rewind [captured_stdout.read, captured_stderr.read] ensure $stdout.reopen orig_stdout $stderr.reopen orig_stderr orig_stdout.close orig_stderr.close captured_stdout.close! captured_stderr.close! end end end ## # Returns details for exception +e+ def exception_details e, msg [ "#{msg}", "Class: <#{e.class}>", "Message: <#{e.message.inspect}>", "---Backtrace---", "#{MiniTest::filter_backtrace(e.backtrace).join("\n")}", "---------------", ].join "\n" end ## # Fails with +msg+ def flunk msg = nil msg ||= "Epic Fail!" assert false, msg end ## # Returns a proc that will output +msg+ along with the default message. def message msg = nil, ending = ".", &default proc { msg = msg.call.chomp(".") if Proc === msg custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? "#{custom_message}#{default.call}#{ending}" } end ## # used for counting assertions def pass msg = nil assert true end ## # Fails if +test+ is a true value def refute test, msg = nil msg ||= "Failed refutation, no message given" not assert(! test, msg) end ## # Fails if +obj+ is empty. def refute_empty obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" } assert_respond_to obj, :empty? refute obj.empty?, msg end ## # Fails if exp == act. # # For floats use refute_in_delta. def refute_equal exp, act, msg = nil msg = message(msg) { "Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}" } refute exp == act, msg end ## # For comparing Floats. Fails if +exp+ is within +delta+ of +act+. # # refute_in_delta Math::PI, (22.0 / 7.0) def refute_in_delta exp, act, delta = 0.001, msg = nil n = (exp - act).abs msg = message(msg) { "Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}" } refute delta >= n, msg end ## # For comparing Floats. Fails if +exp+ and +act+ have a relative error # less than +epsilon+. def refute_in_epsilon a, b, epsilon = 0.001, msg = nil refute_in_delta a, b, a * epsilon, msg end ## # Fails if +collection+ includes +obj+. def refute_includes collection, obj, msg = nil msg = message(msg) { "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}" } assert_respond_to collection, :include? refute collection.include?(obj), msg end ## # Fails if +obj+ is an instance of +cls+. def refute_instance_of cls, obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to not be an instance of #{cls}" } refute obj.instance_of?(cls), msg end ## # Fails if +obj+ is a kind of +cls+. def refute_kind_of cls, obj, msg = nil # TODO: merge with instance_of msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" } refute obj.kind_of?(cls), msg end ## # Fails if +matcher+ =~ +obj+. def refute_match matcher, obj, msg = nil msg = message(msg) {"Expected #{mu_pp matcher} to not match #{mu_pp obj}"} assert_respond_to matcher, :"=~" matcher = Regexp.new Regexp.escape matcher if String === matcher refute matcher =~ obj, msg end ## # Fails if +obj+ is nil. def refute_nil obj, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" } refute obj.nil?, msg end ## # Fails if +o1+ is not +op+ +o2+. Eg: # # refute_operator 1, :>, 2 #=> pass # refute_operator 1, :<, 2 #=> fail def refute_operator o1, op, o2 = (predicate = true; nil), msg = nil return refute_predicate o1, op, msg if predicate msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}"} refute o1.__send__(op, o2), msg end ## # For testing with predicates. # # refute_predicate str, :empty? # # This is really meant for specs and is front-ended by refute_operator: # # str.wont_be :empty? def refute_predicate o1, op, msg = nil msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" } refute o1.__send__(op), msg end ## # Fails if +obj+ responds to the message +meth+. def refute_respond_to obj, meth, msg = nil msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" } refute obj.respond_to?(meth), msg end ## # Fails if +exp+ is the same (by object identity) as +act+. def refute_same exp, act, msg = nil msg = message(msg) { data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] "Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data } refute exp.equal?(act), msg end ## # Skips the current test. Gets listed at the end of the run but # doesn't cause a failure exit code. def skip msg = nil, bt = caller msg ||= "Skipped, no message given" @skip = true raise MiniTest::Skip, msg, bt end ## # Was this testcase skipped? Meant for #teardown. def skipped? defined?(@skip) and @skip end ## # Takes a block and wraps it with the runner's shared mutex. def synchronize Minitest::Unit.runner.synchronize do yield end end end class Unit # :nodoc: VERSION = "4.7.5" # :nodoc: attr_accessor :report, :failures, :errors, :skips # :nodoc: attr_accessor :assertion_count # :nodoc: attr_writer :test_count # :nodoc: attr_accessor :start_time # :nodoc: attr_accessor :help # :nodoc: attr_accessor :verbose # :nodoc: attr_writer :options # :nodoc: ## # :attr: # # if true, installs an "INFO" signal handler (only available to BSD and # OS X users) which prints diagnostic information about the test run. # # This is auto-detected by default but may be overridden by custom # runners. attr_accessor :info_signal ## # Lazy accessor for options. def options @options ||= {} end @@installed_at_exit ||= false @@out = $stdout @@after_tests = [] ## # A simple hook allowing you to run a block of code after _all_ of # the tests are done. Eg: # # MiniTest::Unit.after_tests { p $debugging_info } def self.after_tests &block @@after_tests << block end ## # Registers MiniTest::Unit to run tests at process exit def self.autorun at_exit { # don't run if there was a non-exit exception next if $! and not $!.kind_of? SystemExit # the order here is important. The at_exit handler must be # installed before anyone else gets a chance to install their # own, that way we can be assured that our exit will be last # to run (at_exit stacks). exit_code = nil at_exit { @@after_tests.reverse_each(&:call) exit false if exit_code && exit_code != 0 } exit_code = MiniTest::Unit.new.run ARGV } unless @@installed_at_exit @@installed_at_exit = true end ## # Returns the stream to use for output. def self.output @@out end ## # Sets MiniTest::Unit to write output to +stream+. $stdout is the default # output def self.output= stream @@out = stream end ## # Tells MiniTest::Unit to delegate to +runner+, an instance of a # MiniTest::Unit subclass, when MiniTest::Unit#run is called. def self.runner= runner @@runner = runner end ## # Returns the MiniTest::Unit subclass instance that will be used # to run the tests. A MiniTest::Unit instance is the default # runner. def self.runner @@runner ||= self.new end ## # Return all plugins' run methods (methods that start with "run_"). def self.plugins @@plugins ||= (["run_tests"] + public_instance_methods(false). grep(/^run_/).map { |s| s.to_s }).uniq end ## # Return the IO for output. def output self.class.output end def puts *a # :nodoc: output.puts(*a) end def print *a # :nodoc: output.print(*a) end def test_count # :nodoc: @test_count ||= 0 end ## # Runner for a given +type+ (eg, test vs bench). def _run_anything type suites = TestCase.send "#{type}_suites" return if suites.empty? puts puts "# Running #{type}s:" puts @test_count, @assertion_count = 0, 0 test_count = assertion_count = 0 sync = output.respond_to? :"sync=" # stupid emacs old_sync, output.sync = output.sync, true if sync count = 0 begin start = Time.now results = _run_suites suites, type @test_count = results.inject(0) { |sum, (tc, _)| sum + tc } @assertion_count = results.inject(0) { |sum, (_, ac)| sum + ac } test_count += @test_count assertion_count += @assertion_count t = Time.now - start count += 1 unless @repeat_count puts puts end puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" % [(@repeat_count ? "(#{count}/#{@repeat_count}) " : ""), type, t, @test_count.fdiv(t), @assertion_count.fdiv(t)] end while @repeat_count && count < @repeat_count && report.empty? && failures.zero? && errors.zero? output.sync = old_sync if sync report.each_with_index do |msg, i| puts "\n%3d) %s" % [i + 1, msg] end puts @test_count = test_count @assertion_count = assertion_count status end ## # Runs all the +suites+ for a given +type+. # def _run_suites suites, type suites.map { |suite| _run_suite suite, type } end ## # Run a single +suite+ for a given +type+. def _run_suite suite, type header = "#{type}_suite_header" puts send(header, suite) if respond_to? header filter = options[:filter] || '/./' filter = Regexp.new $1 if filter =~ /\/(.*)\// all_test_methods = suite.send "#{type}_methods" filtered_test_methods = all_test_methods.find_all { |m| filter === m || filter === "#{suite}##{m}" } leakchecker = LeakChecker.new assertions = filtered_test_methods.map { |method| inst = suite.new method inst._assertions = 0 print "#{suite}##{method} = " if @verbose start_time = Time.now if @verbose result = inst.run self print "%.2f s = " % (Time.now - start_time) if @verbose print result puts if @verbose $stdout.flush unless defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # compiler process is wrongly considered as leaked leakchecker.check("#{inst.class}\##{inst.__name__}") end inst._assertions } return assertions.size, assertions.inject(0) { |sum, n| sum + n } end ## # Record the result of a single test. Makes it very easy to gather # information. Eg: # # class StatisticsRecorder < MiniTest::Unit # def record suite, method, assertions, time, error # # ... record the results somewhere ... # end # end # # MiniTest::Unit.runner = StatisticsRecorder.new # # NOTE: record might be sent more than once per test. It will be # sent once with the results from the test itself. If there is a # failure or error in teardown, it will be sent again with the # error or failure. def record suite, method, assertions, time, error end def location e # :nodoc: last_before_assertion = "" e.backtrace.reverse_each do |s| break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/ last_before_assertion = s end last_before_assertion.sub(/:in .*$/, '') end ## # Writes status for failed test +meth+ in +klass+ which finished with # exception +e+ def puke klass, meth, e e = case e when MiniTest::Skip then @skips += 1 return "S" unless @verbose "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" when MiniTest::Assertion then @failures += 1 "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" else @errors += 1 bt = MiniTest::filter_backtrace(e.backtrace).join "\n " "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n" end @report << e e[0, 1] end def initialize # :nodoc: @report = [] @errors = @failures = @skips = 0 @verbose = false @mutex = Thread::Mutex.new @info_signal = Signal.list['INFO'] @repeat_count = nil end def synchronize # :nodoc: if @mutex then @mutex.synchronize { yield } else yield end end def process_args args = [] # :nodoc: options = {} orig_args = args.dup OptionParser.new do |opts| opts.banner = 'minitest options:' opts.version = MiniTest::Unit::VERSION opts.on '-h', '--help', 'Display this help.' do puts opts exit end opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m| options[:seed] = m.to_i end opts.on '-v', '--verbose', "Verbose. Show progress processing files." do options[:verbose] = true end opts.on '-n', '--name PATTERN', "Filter test names on pattern (e.g. /foo/)" do |a| options[:filter] = a end opts.parse! args orig_args -= args end unless options[:seed] then srand options[:seed] = srand % 0xFFFF orig_args << "--seed" << options[:seed].to_s end srand options[:seed] self.verbose = options[:verbose] @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " " options end ## # Begins the full test run. Delegates to +runner+'s #_run method. def run args = [] self.class.runner._run(args) end ## # Top level driver, controls all output and filtering. def _run args = [] args = process_args args # ARGH!! blame test/unit process_args self.options.merge! args puts "Run options: #{help}" self.class.plugins.each do |plugin| send plugin break unless report.empty? end return failures + errors if self.test_count > 0 # or return nil... rescue Interrupt abort 'Interrupted' end ## # Runs test suites matching +filter+. def run_tests _run_anything :test end ## # Writes status to +io+ def status io = self.output format = "%d tests, %d assertions, %d failures, %d errors, %d skips" io.puts format % [test_count, assertion_count, failures, errors, skips] end ## # Provides a simple set of guards that you can use in your tests # to skip execution if it is not applicable. These methods are # mixed into TestCase as both instance and class methods so you # can use them inside or outside of the test methods. # # def test_something_for_mri # skip "bug 1234" if jruby? # # ... # end # # if windows? then # # ... lots of test methods ... # end module Guard ## # Is this running on jruby? def jruby? platform = RUBY_PLATFORM "java" == platform end ## # Is this running on mri? def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE "maglev" == platform end module_function :maglev? ## # Is this running on mri? def mri? platform = RUBY_DESCRIPTION /^ruby/ =~ platform end ## # Is this running on rubinius? def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE "rbx" == platform end ## # Is this running on windows? def windows? platform = RUBY_PLATFORM /mswin|mingw/ =~ platform end end ## # Provides before/after hooks for setup and teardown. These are # meant for library writers, NOT for regular test authors. See # #before_setup for an example. module LifecycleHooks ## # Runs before every test, after setup. This hook is meant for # libraries to extend minitest. It is not meant to be used by # test developers. # # See #before_setup for an example. def after_setup; end ## # Runs before every test, before setup. This hook is meant for # libraries to extend minitest. It is not meant to be used by # test developers. # # As a simplistic example: # # module MyMinitestPlugin # def before_setup # super # # ... stuff to do before setup is run # end # # def after_setup # # ... stuff to do after setup is run # super # end # # def before_teardown # super # # ... stuff to do before teardown is run # end # # def after_teardown # # ... stuff to do after teardown is run # super # end # end # # class MiniTest::Unit::TestCase # include MyMinitestPlugin # end def before_setup; end ## # Runs after every test, before teardown. This hook is meant for # libraries to extend minitest. It is not meant to be used by # test developers. # # See #before_setup for an example. def before_teardown; end ## # Runs after every test, after teardown. This hook is meant for # libraries to extend minitest. It is not meant to be used by # test developers. # # See #before_setup for an example. def after_teardown; end end ## # Subclass TestCase to create your own tests. Typically you'll want a # TestCase subclass per implementation class. # # See MiniTest::Assertions class TestCase include LifecycleHooks include Guard extend Guard attr_reader :__name__ # :nodoc: PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, Interrupt, SystemExit] # :nodoc: ## # Runs the tests reporting the status to +runner+ def run runner trap "INFO" do runner.report.each_with_index do |msg, i| warn "\n%3d) %s" % [i + 1, msg] end warn '' time = runner.start_time ? Time.now - runner.start_time : 0 warn "Current Test: %s#%s %.2fs" % [self.class, self.__name__, time] runner.status $stderr end if runner.info_signal start_time = Time.now result = "" begin @passed = nil self.before_setup self.setup self.after_setup self.run_test self.__name__ result = "." unless io? time = Time.now - start_time runner.record self.class, self.__name__, self._assertions, time, nil @passed = true rescue *PASSTHROUGH_EXCEPTIONS raise rescue Exception => e @passed = Skip === e time = Time.now - start_time runner.record self.class, self.__name__, self._assertions, time, e result = runner.puke self.class, self.__name__, e ensure %w{ before_teardown teardown after_teardown }.each do |hook| begin self.send hook rescue *PASSTHROUGH_EXCEPTIONS raise rescue Exception => e @passed = false runner.record self.class, self.__name__, self._assertions, time, e result = runner.puke self.class, self.__name__, e end end trap 'INFO', 'DEFAULT' if runner.info_signal end result end alias :run_test :__send__ def initialize name # :nodoc: @__name__ = name @__io__ = nil @passed = nil @@current = self # FIX: make thread local end def self.current # :nodoc: @@current # FIX: make thread local end ## # Return the output IO object def io @__io__ = true MiniTest::Unit.output end ## # Have we hooked up the IO yet? def io? @__io__ end def self.reset # :nodoc: @@test_suites = {} end reset ## # Make diffs for this TestCase use #pretty_inspect so that diff # in assert_equal can be more details. NOTE: this is much slower # than the regular inspect but much more usable for complex # objects. def self.make_my_diffs_pretty! require 'pp' define_method :mu_pp do |o| o.pretty_inspect end end def self.inherited klass # :nodoc: @@test_suites[klass] = true super end def self.test_order # :nodoc: :random end def self.test_suites # :nodoc: @@test_suites.keys.sort_by { |ts| ts.name.to_s } end def self.test_methods # :nodoc: methods = public_instance_methods(true).grep(/^test/).map { |m| m.to_s } case self.test_order when :parallel max = methods.size ParallelEach.new methods.sort.sort_by { rand max } when :random then max = methods.size methods.sort.sort_by { rand max } when :alpha, :sorted then methods.sort else raise "Unknown test_order: #{self.test_order.inspect}" end end ## # Returns true if the test passed. def passed? @passed end ## # Runs before every test. Use this to set up before each test # run. def setup; end ## # Runs after every test. Use this to clean up after each test # run. def teardown; end include MiniTest::Assertions end # class TestCase end # class Unit Test = Unit::TestCase end # module MiniTest Minitest = MiniTest # :nodoc: because ugh... I typo this all the time webrick-1.7.0/test/lib/test/000077500000000000000000000000001376456024300156635ustar00rootroot00000000000000webrick-1.7.0/test/lib/test/unit.rb000066400000000000000000001023441376456024300171730ustar00rootroot00000000000000# frozen_string_literal: true begin gem 'minitest', '< 5.0.0' if defined? Gem rescue Gem::LoadError end require 'minitest/unit' require 'test/unit/assertions' require_relative '../envutil' require 'test/unit/testcase' require 'optparse' # See Test::Unit module Test ## # Test::Unit is an implementation of the xUnit testing framework for Ruby. # # If you are writing new test code, please use MiniTest instead of Test::Unit. # # Test::Unit has been left in the standard library to support legacy test # suites. module Unit TEST_UNIT_IMPLEMENTATION = 'test/unit compatibility layer using minitest' # :nodoc: module RunCount # :nodoc: all @@run_count = 0 def self.have_run? @@run_count.nonzero? end def run(*) @@run_count += 1 super end def run_once return if have_run? return if $! # don't run if there was an exception yield end module_function :run_once end module Options # :nodoc: all def initialize(*, &block) @init_hook = block @options = nil super(&nil) end def option_parser @option_parser ||= OptionParser.new end def process_args(args = []) return @options if @options orig_args = args.dup options = {} opts = option_parser setup_options(opts, options) opts.parse!(args) orig_args -= args args = @init_hook.call(args, options) if @init_hook non_options(args, options) @run_options = orig_args @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " " @options = options end private def setup_options(opts, options) opts.separator 'minitest options:' opts.version = MiniTest::Unit::VERSION opts.on '-h', '--help', 'Display this help.' do puts opts exit end opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m| options[:seed] = m end opts.on '-v', '--verbose', "Verbose. Show progress processing files." do options[:verbose] = true self.verbose = options[:verbose] end opts.on '-n', '--name PATTERN', "Filter test method names on pattern: /REGEXP/, !/REGEXP/ or STRING" do |a| (options[:filter] ||= []) << a end opts.on '--test-order=random|alpha|sorted', [:random, :alpha, :sorted] do |a| MiniTest::Unit::TestCase.test_order = a end end def non_options(files, options) filter = options[:filter] if filter pos_pat = /\A\/(.*)\/\z/ neg_pat = /\A!\/(.*)\/\z/ negative, positive = filter.partition {|s| neg_pat =~ s} if positive.empty? filter = nil elsif negative.empty? and positive.size == 1 and pos_pat !~ positive[0] filter = positive[0] else filter = Regexp.union(*positive.map! {|s| Regexp.new(s[pos_pat, 1] || "\\A#{Regexp.quote(s)}\\z")}) end unless negative.empty? negative = Regexp.union(*negative.map! {|s| Regexp.new(s[neg_pat, 1])}) filter = /\A(?=.*#{filter})(?!.*#{negative})/ end if Regexp === filter # bypass conversion in minitest def filter.=~(other) # :nodoc: super unless Regexp === other end end options[:filter] = filter end true end end module Parallel # :nodoc: all def process_args(args = []) return @options if @options options = super if @options[:parallel] @files = args end options end def non_options(files, options) @jobserver = nil if !options[:parallel] and /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ ENV["MAKEFLAGS"] begin r = IO.for_fd($1.to_i(10), "rb", autoclose: false) w = IO.for_fd($2.to_i(10), "wb", autoclose: false) rescue r.close if r nil else @jobserver = [r, w] options[:parallel] ||= 1 end end super end def status(*args) result = super raise @interrupt if @interrupt result end private def setup_options(opts, options) super opts.separator "parallel test options:" options[:retry] = true opts.on '-j N', '--jobs N', /\A(t)?(\d+)\z/, "Allow run tests with N jobs at once" do |_, t, a| options[:testing] = true & t # For testing options[:parallel] = a.to_i end opts.on '--separate', "Restart job process after one testcase has done" do options[:parallel] ||= 1 options[:separate] = true end opts.on '--retry', "Retry running testcase when --jobs specified" do options[:retry] = true end opts.on '--no-retry', "Disable --retry" do options[:retry] = false end opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a| options[:ruby] = a.split(/ /).reject(&:empty?) end end class Worker def self.launch(ruby,args=[]) io = IO.popen([*ruby, "-W1", "#{File.dirname(__FILE__)}/unit/parallel.rb", *args], "rb+") new(io, io.pid, :waiting) end attr_reader :quit_called def initialize(io, pid, status) @io = io @pid = pid @status = status @file = nil @real_file = nil @loadpath = [] @hooks = {} @quit_called = false end def puts(*args) @io.puts(*args) end def run(task,type) @file = File.basename(task, ".rb") @real_file = task begin puts "loadpath #{[Marshal.dump($:-@loadpath)].pack("m0")}" @loadpath = $:.dup puts "run #{task} #{type}" @status = :prepare rescue Errno::EPIPE died rescue IOError raise unless /stream closed|closed stream/ =~ $!.message died end end def hook(id,&block) @hooks[id] ||= [] @hooks[id] << block self end def read res = (@status == :quit) ? @io.read : @io.gets res && res.chomp end def close @io.close unless @io.closed? self rescue IOError end def quit return if @io.closed? @quit_called = true @io.puts "quit" end def kill Process.kill(:KILL, @pid) rescue Errno::ESRCH end def died(*additional) @status = :quit @io.close status = $? if status and status.signaled? additional[0] ||= SignalException.new(status.termsig) end call_hook(:dead,*additional) end def to_s if @file and @status != :ready "#{@pid}=#{@file}" else "#{@pid}:#{@status.to_s.ljust(7)}" end end attr_reader :io, :pid attr_accessor :status, :file, :real_file, :loadpath private def call_hook(id,*additional) @hooks[id] ||= [] @hooks[id].each{|hook| hook[self,additional] } self end end def flush_job_tokens if @jobserver r, w = @jobserver.shift(2) @jobserver = nil w << @job_tokens.slice!(0..-1) r.close w.close end end def after_worker_down(worker, e=nil, c=false) return unless @options[:parallel] return if @interrupt flush_job_tokens warn e if e real_file = worker.real_file and warn "running file: #{real_file}" @need_quit = true warn "" warn "Some worker was crashed. It seems ruby interpreter's bug" warn "or, a bug of test/unit/parallel.rb. try again without -j" warn "option." warn "" STDERR.flush exit c end def after_worker_quit(worker) return unless @options[:parallel] return if @interrupt worker.close if @jobserver and (token = @job_tokens.slice!(0)) @jobserver[1] << token end @workers.delete(worker) @dead_workers << worker @ios = @workers.map(&:io) end def launch_worker begin worker = Worker.launch(@options[:ruby], @run_options) rescue => e abort "ERROR: Failed to launch job process - #{e.class}: #{e.message}" end worker.hook(:dead) do |w,info| after_worker_quit w after_worker_down w, *info if !info.empty? && !worker.quit_called end @workers << worker @ios << worker.io @workers_hash[worker.io] = worker worker end def delete_worker(worker) @workers_hash.delete worker.io @workers.delete worker @ios.delete worker.io end def quit_workers return if @workers.empty? @workers.reject! do |worker| begin Timeout.timeout(1) do worker.quit end rescue Errno::EPIPE rescue Timeout::Error end worker.close end return if @workers.empty? begin Timeout.timeout(0.2 * @workers.size) do Process.waitall end rescue Timeout::Error @workers.each do |worker| worker.kill end @worker.clear end end FakeClass = Struct.new(:name) def fake_class(name) (@fake_classes ||= {})[name] ||= FakeClass.new(name) end def deal(io, type, result, rep, shutting_down = false) worker = @workers_hash[io] cmd = worker.read cmd.sub!(/\A\.+/, '') if cmd # read may return nil case cmd when '' # just only dots, ignore when /^okay$/ worker.status = :running when /^ready(!)?$/ bang = $1 worker.status = :ready unless task = @tasks.shift worker.quit return nil end if @options[:separate] and not bang worker.quit worker = add_worker end worker.run(task, type) @test_count += 1 jobs_status(worker) when /^done (.+?)$/ begin r = Marshal.load($1.unpack("m")[0]) rescue print "unknown object: #{$1.unpack("m")[0].dump}" return true end result << r[0..1] unless r[0..1] == [nil,nil] rep << {file: worker.real_file, report: r[2], result: r[3], testcase: r[5]} $:.push(*r[4]).uniq! jobs_status(worker) if @options[:job_status] == :replace return true when /^record (.+?)$/ begin r = Marshal.load($1.unpack("m")[0]) rescue => e print "unknown record: #{e.message} #{$1.unpack("m")[0].dump}" return true end record(fake_class(r[0]), *r[1..-1]) when /^p (.+?)$/ del_jobs_status print $1.unpack("m")[0] jobs_status(worker) if @options[:job_status] == :replace when /^after (.+?)$/ @warnings << Marshal.load($1.unpack("m")[0]) when /^bye (.+?)$/ after_worker_down worker, Marshal.load($1.unpack("m")[0]) when /^bye$/, nil if shutting_down || worker.quit_called after_worker_quit worker else after_worker_down worker end else print "unknown command: #{cmd.dump}\n" end return false end def _run_parallel suites, type, result if @options[:parallel] < 1 warn "Error: parameter of -j option should be greater than 0." return end # Require needed things for parallel running require 'thread' require 'timeout' @tasks = @files.dup # Array of filenames. @need_quit = false @dead_workers = [] # Array of dead workers. @warnings = [] @total_tests = @tasks.size.to_s(10) rep = [] # FIXME: more good naming @workers = [] # Array of workers. @workers_hash = {} # out-IO => worker @ios = [] # Array of worker IOs @job_tokens = String.new(encoding: Encoding::ASCII_8BIT) if @jobserver begin [@tasks.size, @options[:parallel]].min.times {launch_worker} while _io = IO.select(@ios)[0] break if _io.any? do |io| @need_quit or (deal(io, type, result, rep).nil? and !@workers.any? {|x| [:running, :prepare].include? x.status}) end if @jobserver and @job_tokens and !@tasks.empty? and !@workers.any? {|x| x.status == :ready} t = @jobserver[0].read_nonblock([@tasks.size, @options[:parallel]].min, exception: false) if String === t @job_tokens << t t.size.times {launch_worker} end end end rescue Interrupt => ex @interrupt = ex return result ensure if @interrupt @ios.select!{|x| @workers_hash[x].status == :running } while !@ios.empty? && (__io = IO.select(@ios,[],[],10)) __io[0].reject! {|io| deal(io, type, result, rep, true)} end end quit_workers flush_job_tokens unless @interrupt || !@options[:retry] || @need_quit parallel = @options[:parallel] @options[:parallel] = false suites, rep = rep.partition {|r| r[:testcase] && r[:file] && r[:report].any? {|e| !e[2].is_a?(MiniTest::Skip)}} suites.map {|r| r[:file]}.uniq.each {|file| require file} suites.map! {|r| eval("::"+r[:testcase])} del_status_line or puts unless suites.empty? puts "\n""Retrying..." _run_suites(suites, type) end @options[:parallel] = parallel end unless @options[:retry] del_status_line or puts end unless rep.empty? rep.each do |r| r[:report].each do |f| puke(*f) if f end end if @options[:retry] @errors += rep.map{|x| x[:result][0] }.inject(:+) @failures += rep.map{|x| x[:result][1] }.inject(:+) @skips += rep.map{|x| x[:result][2] }.inject(:+) end end unless @warnings.empty? warn "" @warnings.uniq! {|w| w[1].message} @warnings.each do |w| warn "#{w[0]}: #{w[1].message} (#{w[1].class})" end warn "" end end end def _run_suites suites, type _prepare_run(suites, type) @interrupt = nil result = [] GC.start if @options[:parallel] _run_parallel suites, type, result else suites.each {|suite| begin result << _run_suite(suite, type) rescue Interrupt => e @interrupt = e break end } end del_status_line result end end module Skipping # :nodoc: all def failed(s) super if !s or @options[:hide_skip] end private def setup_options(opts, options) super opts.separator "skipping options:" options[:hide_skip] = true opts.on '-q', '--hide-skip', 'Hide skipped tests' do options[:hide_skip] = true end opts.on '--show-skip', 'Show skipped tests' do options[:hide_skip] = false end end private def _run_suites(suites, type) result = super report.reject!{|r| r.start_with? "Skipped:" } if @options[:hide_skip] report.sort_by!{|r| r.start_with?("Skipped:") ? 0 : \ (r.start_with?("Failure:") ? 1 : 2) } failed(nil) result end end module Statistics def update_list(list, rec, max) if i = list.empty? ? 0 : list.bsearch_index {|*a| yield(*a)} list[i, 0] = [rec] list[max..-1] = [] if list.size >= max end end def record(suite, method, assertions, time, error) if @options.values_at(:longest, :most_asserted).any? @tops ||= {} rec = [suite.name, method, assertions, time, error] if max = @options[:longest] update_list(@tops[:longest] ||= [], rec, max) {|_,_,_,t,_|t 0 end $stdout.flush if flush @status_line_size = 0 end def add_status(line) @status_line_size ||= 0 if @options[:job_status] == :replace line = line[0...(terminal_width-@status_line_size)] end print line @status_line_size += line.size end def jobs_status(worker) return if !@options[:job_status] or @options[:verbose] if @options[:job_status] == :replace status_line = @workers.map(&:to_s).join(" ") else status_line = worker.to_s end update_status(status_line) or (puts; nil) end def del_jobs_status return unless @options[:job_status] == :replace && @status_line_size.nonzero? del_status_line end def output (@output ||= nil) || super end def _prepare_run(suites, type) options[:job_status] ||= :replace if @tty && !@verbose case options[:color] when :always color = true when :auto, nil color = (@tty || @options[:job_status] == :replace) && /dumb/ !~ ENV["TERM"] else color = false end if color # dircolors-like style colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} begin File.read(File.join(__dir__, "../../colors")).scan(/(\w+)=([^:\n]*)/) do |n, c| colors[n] ||= c end rescue end @passed_color = "\e[;#{colors["pass"] || "32"}m" @failed_color = "\e[;#{colors["fail"] || "31"}m" @skipped_color = "\e[;#{colors["skip"] || "33"}m" @reset_color = "\e[m" else @passed_color = @failed_color = @skipped_color = @reset_color = "" end if color or @options[:job_status] == :replace @verbose = !options[:parallel] end @output = Output.new(self) unless @options[:testing] filter = options[:filter] type = "#{type}_methods" total = if filter suites.inject(0) {|n, suite| n + suite.send(type).grep(filter).size} else suites.inject(0) {|n, suite| n + suite.send(type).size} end @test_count = 0 @total_tests = total.to_s(10) end def new_test(s) @test_count += 1 update_status(s) end def update_status(s) count = @test_count.to_s(10).rjust(@total_tests.size) del_status_line(false) print(@passed_color) add_status("[#{count}/#{@total_tests}]") print(@reset_color) add_status(" #{s}") $stdout.print "\r" if @options[:job_status] == :replace and !@verbose $stdout.flush end def _print(s); $stdout.print(s); end def succeed; del_status_line; end def failed(s) return if s and @options[:job_status] != :replace sep = "\n" @report_count ||= 0 report.each do |msg| if msg.start_with? "Skipped:" if @options[:hide_skip] del_status_line next end color = @skipped_color else color = @failed_color end msg = msg.split(/$/, 2) $stdout.printf("%s%s%3d) %s%s%s\n", sep, color, @report_count += 1, msg[0], @reset_color, msg[1]) sep = nil end report.clear end def initialize super @tty = $stdout.tty? end def run(*args) result = super puts "\nruby -v: #{RUBY_DESCRIPTION}" result end private def setup_options(opts, options) super opts.separator "status line options:" options[:job_status] = nil opts.on '--jobs-status [TYPE]', [:normal, :replace, :none], "Show status of jobs every file; Disabled when --jobs isn't specified." do |type| options[:job_status] = (type || :normal if type != :none) end opts.on '--color[=WHEN]', [:always, :never, :auto], "colorize the output. WHEN defaults to 'always'", "or can be 'never' or 'auto'." do |c| options[:color] = c || :always end opts.on '--tty[=WHEN]', [:yes, :no], "force to output tty control. WHEN defaults to 'yes'", "or can be 'no'." do |c| @tty = c != :no end end class Output < Struct.new(:runner) # :nodoc: all def puts(*a) $stdout.puts(*a) unless a.empty? end def respond_to_missing?(*a) $stdout.respond_to?(*a) end def method_missing(*a, &b) $stdout.__send__(*a, &b) end def print(s) case s when /\A(.*\#.*) = \z/ runner.new_test($1) when /\A(.* s) = \z/ runner.add_status(" = #$1") when /\A\.+\z/ runner.succeed when /\A[EFS]\z/ runner.failed(s) else $stdout.print(s) end end end end module LoadPathOption # :nodoc: all def non_options(files, options) begin require "rbconfig" rescue LoadError warn "#{caller(1)[0]}: warning: Parallel running disabled because can't get path to ruby; run specify with --ruby argument" options[:parallel] = nil else options[:ruby] ||= [RbConfig.ruby] end super end def setup_options(parser, options) super parser.separator "load path options:" parser.on '-Idirectory', 'Add library load path' do |dirs| dirs.split(':').each { |d| $LOAD_PATH.unshift d } end end end module GlobOption # :nodoc: all @@testfile_prefix = "test" @@testfile_suffix = "test" def setup_options(parser, options) super parser.separator "globbing options:" parser.on '-b', '--basedir=DIR', 'Base directory of test suites.' do |dir| options[:base_directory] = dir end parser.on '-x', '--exclude REGEXP', 'Exclude test files on pattern.' do |pattern| (options[:reject] ||= []) << pattern end end def non_options(files, options) paths = [options.delete(:base_directory), nil].uniq if reject = options.delete(:reject) reject_pat = Regexp.union(reject.map {|r| %r"#{r}"}) end files.map! {|f| f = f.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR ((paths if /\A\.\.?(?:\z|\/)/ !~ f) || [nil]).any? do |prefix| if prefix path = f.empty? ? prefix : "#{prefix}/#{f}" else next if f.empty? path = f end if !(match = (Dir["#{path}/**/#{@@testfile_prefix}_*.rb"] + Dir["#{path}/**/*_#{@@testfile_suffix}.rb"]).uniq).empty? if reject match.reject! {|n| n[(prefix.length+1)..-1] if prefix reject_pat =~ n } end break match elsif !reject or reject_pat !~ f and File.exist? path break path end end or raise ArgumentError, "file not found: #{f}" } files.flatten! super(files, options) end end module GCStressOption # :nodoc: all def setup_options(parser, options) super parser.separator "GC options:" parser.on '--[no-]gc-stress', 'Set GC.stress as true' do |flag| options[:gc_stress] = flag end end def non_options(files, options) if options.delete(:gc_stress) MiniTest::Unit::TestCase.class_eval do oldrun = instance_method(:run) define_method(:run) do |runner| begin gc_stress, GC.stress = GC.stress, true oldrun.bind(self).call(runner) ensure GC.stress = gc_stress end end end end super end end module RequireFiles # :nodoc: all def non_options(files, options) return false if !super errors = {} result = false files.each {|f| d = File.dirname(path = File.realpath(f)) unless $:.include? d $: << d end begin require path unless options[:parallel] result = true rescue LoadError next if errors[$!.message] errors[$!.message] = true puts "#{f}: #{$!}" end } result end end module RepeatOption # :nodoc: all def setup_options(parser, options) super options[:repeat_count] = nil parser.separator "repeat options:" parser.on '--repeat-count=NUM', "Number of times to repeat", Integer do |n| options[:repeat_count] = n end end def _run_anything(type) @repeat_count = @options[:repeat_count] super end end module ExcludesOption # :nodoc: all class ExcludedMethods < Struct.new(:excludes) def exclude(name, reason) excludes[name] = reason end def exclude_from(klass) excludes = self.excludes pattern = excludes.keys.grep(Regexp).tap {|k| break (Regexp.new(k.join('|')) unless k.empty?) } klass.class_eval do public_instance_methods(false).each do |method| if excludes[method] or (pattern and pattern =~ method) remove_method(method) end end public_instance_methods(true).each do |method| if excludes[method] or (pattern and pattern =~ method) undef_method(method) end end end end def self.load(dirs, name) return unless dirs and name instance = nil dirs.each do |dir| path = File.join(dir, name.gsub(/::/, '/') + ".rb") begin src = File.read(path) rescue Errno::ENOENT nil else instance ||= new({}) instance.instance_eval(src, path) end end instance end end def setup_options(parser, options) super if excludes = ENV["EXCLUDES"] excludes = excludes.split(File::PATH_SEPARATOR) end options[:excludes] = excludes || [] parser.separator "excludes options:" parser.on '-X', '--excludes-dir DIRECTORY', "Directory name of exclude files" do |d| options[:excludes].concat d.split(File::PATH_SEPARATOR) end end def _run_suite(suite, type) if ex = ExcludedMethods.load(@options[:excludes], suite.name) ex.exclude_from(suite) end super end end module SubprocessOption def setup_options(parser, options) super parser.separator "subprocess options:" parser.on '--subprocess-timeout-scale NUM', "Scale subprocess timeout", Float do |scale| raise OptionParser::InvalidArgument, "timeout scale must be positive" unless scale > 0 options[:timeout_scale] = scale end if scale = options[:timeout_scale] or (scale = ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"] and (scale = scale.to_f) > 0) EnvUtil.subprocess_timeout_scale = scale end end end class Runner < MiniTest::Unit # :nodoc: all include Test::Unit::Options include Test::Unit::StatusLine include Test::Unit::Parallel include Test::Unit::Statistics include Test::Unit::Skipping include Test::Unit::GlobOption include Test::Unit::RepeatOption include Test::Unit::LoadPathOption include Test::Unit::GCStressOption include Test::Unit::ExcludesOption include Test::Unit::SubprocessOption include Test::Unit::RunCount class << self; undef autorun; end @@stop_auto_run = false def self.autorun at_exit { Test::Unit::RunCount.run_once { exit(Test::Unit::Runner.new.run(ARGV) || true) } unless @@stop_auto_run } unless @@installed_at_exit @@installed_at_exit = true end alias mini_run_suite _run_suite # Overriding of MiniTest::Unit#puke def puke klass, meth, e # TODO: # this overriding is for minitest feature that skip messages are # hidden when not verbose (-v), note this is temporally. n = report.size rep = super if MiniTest::Skip === e and /no message given\z/ =~ e.message report.slice!(n..-1) rep = "." end rep end end class AutoRunner # :nodoc: all class Runner < Test::Unit::Runner include Test::Unit::RequireFiles end attr_accessor :to_run, :options def initialize(force_standalone = false, default_dir = nil, argv = ARGV) @force_standalone = force_standalone @runner = Runner.new do |files, options| options[:base_directory] ||= default_dir files << default_dir if files.empty? and default_dir @to_run = files yield self if block_given? files end Runner.runner = @runner @options = @runner.option_parser if @force_standalone @options.banner.sub!(/\[options\]/, '\& tests...') end @argv = argv end def process_args(*args) @runner.process_args(*args) !@to_run.empty? end def run if @force_standalone and not process_args(@argv) abort @options.banner end @runner.run(@argv) || true end def self.run(*args) new(*args).run end end class ProxyError < StandardError # :nodoc: all def initialize(ex) @message = ex.message @backtrace = ex.backtrace end attr_accessor :message, :backtrace end end end module MiniTest # :nodoc: all class Unit end end class MiniTest::Unit::TestCase # :nodoc: all test_order = self.test_order class << self attr_writer :test_order undef test_order end def self.test_order defined?(@test_order) ? @test_order : superclass.test_order end self.test_order = test_order undef run_test RUN_TEST_TRACE = "#{__FILE__}:#{__LINE__+3}:in `run_test'".freeze def run_test(name) progname, $0 = $0, "#{$0}: #{self.class}##{name}" self.__send__(name) ensure $@.delete(RUN_TEST_TRACE) if $@ $0 = progname end end Test::Unit::Runner.autorun webrick-1.7.0/test/lib/test/unit/000077500000000000000000000000001376456024300166425ustar00rootroot00000000000000webrick-1.7.0/test/lib/test/unit/assertions.rb000066400000000000000000000751421376456024300213720ustar00rootroot00000000000000# frozen_string_literal: true require 'minitest/unit' require 'pp' module Test module Unit module Assertions include MiniTest::Assertions def mu_pp(obj) #:nodoc: obj.pretty_inspect.chomp end MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: # :call-seq: # assert(test, [failure_message]) # #Tests if +test+ is true. # #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used #as the failure message. Otherwise, the result of calling +msg+ will be #used as the message if the assertion fails. # #If no +msg+ is given, a default message will be used. # # assert(false, "This was expected to be true") def assert(test, *msgs) case msg = msgs.first when String, Proc when nil msgs.shift else bt = caller.reject { |s| s.start_with?(MINI_DIR) } raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt end unless msgs.empty? super end # :call-seq: # assert_block( failure_message = nil ) # #Tests the result of the given block. If the block does not return true, #the assertion will fail. The optional +failure_message+ argument is the same as in #Assertions#assert. # # assert_block do # [1, 2, 3].any? { |num| num < 1 } # end def assert_block(*msgs) assert yield, *msgs end # :call-seq: # assert_raise( *args, &block ) # #Tests if the given block raises an exception. Acceptable exception #types may be given as optional arguments. If the last argument is a #String, it will be used as the error message. # # assert_raise do #Fails, no Exceptions are raised # end # # assert_raise NameError do # puts x #Raises NameError, so assertion succeeds # end def assert_raise(*exp, &b) case exp.last when String, Proc msg = exp.pop end begin yield rescue MiniTest::Skip => e return e if exp.include? MiniTest::Skip raise e rescue Exception => e expected = exp.any? { |ex| if ex.instance_of? Module then e.kind_of? ex else e.instance_of? ex end } assert expected, proc { exception_details(e, message(msg) {"#{mu_pp(exp)} exception expected, not"}.call) } return e end exp = exp.first if exp.size == 1 flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) end def assert_raises(*exp, &b) raise NoMethodError, "use assert_raise", caller end # :call-seq: # assert_raise_with_message(exception, expected, msg = nil, &block) # #Tests if the given block raises an exception with the expected #message. # # assert_raise_with_message(RuntimeError, "foo") do # nil #Fails, no Exceptions are raised # end # # assert_raise_with_message(RuntimeError, "foo") do # raise ArgumentError, "foo" #Fails, different Exception is raised # end # # assert_raise_with_message(RuntimeError, "foo") do # raise "bar" #Fails, RuntimeError is raised but the message differs # end # # assert_raise_with_message(RuntimeError, "foo") do # raise "foo" #Raises RuntimeError with the message, so assertion succeeds # end def assert_raise_with_message(exception, expected, msg = nil, &block) case expected when String assert = :assert_equal when Regexp assert = :assert_match else raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" end ex = m = nil EnvUtil.with_default_internal(expected.encoding) do ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do yield end m = ex.message end msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} if assert == :assert_equal assert_equal(expected, m, msg) else msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } assert expected =~ m, msg block.binding.eval("proc{|_|$~=_}").call($~) end ex end # :call-seq: # assert_nothing_raised( *args, &block ) # #If any exceptions are given as arguments, the assertion will #fail if one of those exceptions are raised. Otherwise, the test fails #if any exceptions are raised. # #The final argument may be a failure message. # # assert_nothing_raised RuntimeError do # raise Exception #Assertion passes, Exception is not a RuntimeError # end # # assert_nothing_raised do # raise Exception #Assertion fails # end def assert_nothing_raised(*args) self._assertions += 1 if Module === args.last msg = nil else msg = args.pop end begin line = __LINE__; yield rescue MiniTest::Skip raise rescue Exception => e bt = e.backtrace as = e.instance_of?(MiniTest::Assertion) if as ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o bt.reject! {|ln| ans =~ ln} end if ((args.empty? && !as) || args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) msg = message(msg) { "Exception raised:\n<#{mu_pp(e)}>" } raise MiniTest::Assertion, msg.call, bt else raise end end end # :call-seq: # assert_nothing_thrown( failure_message = nil, &block ) # #Fails if the given block uses a call to Kernel#throw, and #returns the result of the block otherwise. # #An optional failure message may be provided as the final argument. # # assert_nothing_thrown "Something was thrown!" do # throw :problem? # end def assert_nothing_thrown(msg=nil) begin ret = yield rescue ArgumentError => error raise error if /\Auncaught throw (.+)\z/m !~ error.message msg = message(msg) { "<#{$1}> was thrown when nothing was expected" } flunk(msg) end assert(true, "Expected nothing to be thrown") ret end # :call-seq: # assert_throw( tag, failure_message = nil, &block ) # #Fails unless the given block throws +tag+, returns the caught #value otherwise. # #An optional failure message may be provided as the final argument. # # tag = Object.new # assert_throw(tag, "#{tag} was not thrown!") do # throw tag # end def assert_throw(tag, msg = nil) ret = catch(tag) do begin yield(tag) rescue UncaughtThrowError => e thrown = e.tag end msg = message(msg) { "Expected #{mu_pp(tag)} to have been thrown"\ "#{%Q[, not #{thrown}] if thrown}" } assert(false, msg) end assert(true) ret end # :call-seq: # assert_equal( expected, actual, failure_message = nil ) # #Tests if +expected+ is equal to +actual+. # #An optional failure message may be provided as the final argument. def assert_equal(exp, act, msg = nil) msg = message(msg) { exp_str = mu_pp(exp) act_str = mu_pp(act) exp_comment = '' act_comment = '' if exp_str == act_str if (exp.is_a?(String) && act.is_a?(String)) || (exp.is_a?(Regexp) && act.is_a?(Regexp)) exp_comment = " (#{exp.encoding})" act_comment = " (#{act.encoding})" elsif exp.is_a?(Float) && act.is_a?(Float) exp_str = "%\#.#{Float::DIG+2}g" % exp act_str = "%\#.#{Float::DIG+2}g" % act elsif exp.is_a?(Time) && act.is_a?(Time) if exp.subsec * 1000_000_000 == exp.nsec exp_comment = " (#{exp.nsec}[ns])" else exp_comment = " (subsec=#{exp.subsec})" end if act.subsec * 1000_000_000 == act.nsec act_comment = " (#{act.nsec}[ns])" else act_comment = " (subsec=#{act.subsec})" end elsif exp.class != act.class # a subclass of Range, for example. exp_comment = " (#{exp.class})" act_comment = " (#{act.class})" end elsif !Encoding.compatible?(exp_str, act_str) if exp.is_a?(String) && act.is_a?(String) exp_str = exp.dump act_str = act.dump exp_comment = " (#{exp.encoding})" act_comment = " (#{act.encoding})" else exp_str = exp_str.dump act_str = act_str.dump end end "<#{exp_str}>#{exp_comment} expected but was\n<#{act_str}>#{act_comment}" } assert(exp == act, msg) end # :call-seq: # assert_not_nil( expression, failure_message = nil ) # #Tests if +expression+ is not nil. # #An optional failure message may be provided as the final argument. def assert_not_nil(exp, msg=nil) msg = message(msg) { "<#{mu_pp(exp)}> expected to not be nil" } assert(!exp.nil?, msg) end # :call-seq: # assert_not_equal( expected, actual, failure_message = nil ) # #Tests if +expected+ is not equal to +actual+. # #An optional failure message may be provided as the final argument. def assert_not_equal(exp, act, msg=nil) msg = message(msg) { "<#{mu_pp(exp)}> expected to be != to\n<#{mu_pp(act)}>" } assert(exp != act, msg) end # :call-seq: # assert_no_match( regexp, string, failure_message = nil ) # #Tests if the given Regexp does not match a given String. # #An optional failure message may be provided as the final argument. def assert_no_match(regexp, string, msg=nil) assert_instance_of(Regexp, regexp, "The first argument to assert_no_match should be a Regexp.") self._assertions -= 1 msg = message(msg) { "<#{mu_pp(regexp)}> expected to not match\n<#{mu_pp(string)}>" } assert(regexp !~ string, msg) end # :call-seq: # assert_not_same( expected, actual, failure_message = nil ) # #Tests if +expected+ is not the same object as +actual+. #This test uses Object#equal? to test equality. # #An optional failure message may be provided as the final argument. # # assert_not_same("x", "x") #Succeeds def assert_not_same(expected, actual, message="") msg = message(msg) { build_message(message, < with id expected to not be equal\\? to with id . EOT assert(!actual.equal?(expected), msg) end # :call-seq: # assert_respond_to( object, method, failure_message = nil ) # #Tests if the given Object responds to +method+. # #An optional failure message may be provided as the final argument. # # assert_respond_to("hello", :reverse) #Succeeds # assert_respond_to("hello", :does_not_exist) #Fails def assert_respond_to(obj, (meth, *priv), msg = nil) unless priv.empty? msg = message(msg) { "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" } return assert obj.respond_to?(meth, *priv), msg end #get rid of overcounting if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) return if obj.respond_to?(meth) end super(obj, meth, msg) end # :call-seq: # assert_not_respond_to( object, method, failure_message = nil ) # #Tests if the given Object does not respond to +method+. # #An optional failure message may be provided as the final argument. # # assert_not_respond_to("hello", :reverse) #Fails # assert_not_respond_to("hello", :does_not_exist) #Succeeds def assert_not_respond_to(obj, (meth, *priv), msg = nil) unless priv.empty? msg = message(msg) { "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" } return assert !obj.respond_to?(meth, *priv), msg end #get rid of overcounting if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) return unless obj.respond_to?(meth) end refute_respond_to(obj, meth, msg) end # :call-seq: # assert_send( +send_array+, failure_message = nil ) # # Passes if the method send returns a true value. # # +send_array+ is composed of: # * A receiver # * A method # * Arguments to the method # # Example: # assert_send(["Hello world", :include?, "Hello"]) # -> pass # assert_send(["Hello world", :include?, "Goodbye"]) # -> fail def assert_send send_ary, m = nil recv, msg, *args = send_ary m = message(m) { if args.empty? argsstr = "" else (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)') end "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return true" } assert recv.__send__(msg, *args), m end # :call-seq: # assert_not_send( +send_array+, failure_message = nil ) # # Passes if the method send doesn't return a true value. # # +send_array+ is composed of: # * A receiver # * A method # * Arguments to the method # # Example: # assert_not_send([[1, 2], :member?, 1]) # -> fail # assert_not_send([[1, 2], :member?, 4]) # -> pass def assert_not_send send_ary, m = nil recv, msg, *args = send_ary m = message(m) { if args.empty? argsstr = "" else (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)') end "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return false" } assert !recv.__send__(msg, *args), m end ms = instance_methods(true).map {|sym| sym.to_s } ms.grep(/\Arefute_/) do |m| mname = ('assert_not_'.dup << m.to_s[/.*?_(.*)/, 1]) alias_method(mname, m) unless ms.include? mname end alias assert_include assert_includes alias assert_not_include assert_not_includes def assert_all?(obj, m = nil, &blk) failed = [] obj.each do |*a, &b| unless blk.call(*a, &b) failed << (a.size > 1 ? a : a[0]) end end assert(failed.empty?, message(m) {failed.pretty_inspect}) end def assert_not_all?(obj, m = nil, &blk) failed = [] obj.each do |*a, &b| if blk.call(*a, &b) failed << a.size > 1 ? a : a[0] end end assert(failed.empty?, message(m) {failed.pretty_inspect}) end # compatibility with test-unit alias pend skip if defined?(RubyVM::InstructionSequence) def syntax_check(code, fname, line) code = code.dup.force_encoding(Encoding::UTF_8) RubyVM::InstructionSequence.compile(code, fname, fname, line) :ok end else def syntax_check(code, fname, line) code = code.b code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" } code = code.force_encoding(Encoding::UTF_8) catch {|tag| eval(code, binding, fname, line - 1)} end end def prepare_syntax_check(code, fname = caller_locations(2, 1)[0], mesg = fname.to_s, verbose: nil) verbose, $VERBOSE = $VERBOSE, verbose case when Array === fname fname, line = *fname when defined?(fname.path) && defined?(fname.lineno) fname, line = fname.path, fname.lineno else line = 1 end yield(code, fname, line, mesg) ensure $VERBOSE = verbose end def assert_valid_syntax(code, *args) prepare_syntax_check(code, *args) do |src, fname, line, mesg| yield if defined?(yield) assert_nothing_raised(SyntaxError, mesg) do assert_equal(:ok, syntax_check(src, fname, line), mesg) end end end def assert_syntax_error(code, error, *args) prepare_syntax_check(code, *args) do |src, fname, line, mesg| yield if defined?(yield) e = assert_raise(SyntaxError, mesg) do syntax_check(src, fname, line) end assert_match(error, e.message, mesg) e end end def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) if child_env child_env = [child_env] else child_env = [] end out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) assert !status.signaled?, FailDesc[status, message, out] end FailDesc = proc do |status, message = "", out = ""| pid = status.pid now = Time.now faildesc = proc do if signo = status.termsig signame = Signal.signame(signo) sigdesc = "signal #{signo}" end log = EnvUtil.diagnostic_reports(signame, pid, now) if signame sigdesc = "SIG#{signame} (#{sigdesc})" end if status.coredump? sigdesc = "#{sigdesc} (core dumped)" end full_message = ''.dup message = message.call if Proc === message if message and !message.empty? full_message << message << "\n" end full_message << "pid #{pid}" full_message << " exit #{status.exitstatus}" if status.exited? full_message << " killed by #{sigdesc}" if sigdesc if out and !out.empty? full_message << "\n" << out.b.gsub(/^/, '| ') full_message.sub!(/(? marshal_error ignore_stderr = nil end if res if bt = res.backtrace bt.each do |l| l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} end bt.concat(caller) else res.set_backtrace(caller) end raise res unless SystemExit === res end # really is it succeed? unless ignore_stderr # the body of assert_separately must not output anything to detect error assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) end assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) raise marshal_error if marshal_error end def assert_warning(pat, msg = nil) stderr = EnvUtil.verbose_warning { EnvUtil.with_default_internal(pat.encoding) { yield } } msg = message(msg) {diff pat, stderr} assert(pat === stderr, msg) end def assert_warn(*args) assert_warning(*args) {$VERBOSE = false; yield} end def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) require_relative '../../memory_status' raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" token_dump = token.dump token_re = Regexp.quote(token) envs = args.shift if Array === args and Hash === args.first args = [ "--disable=gems", "-r", File.expand_path("../../../memory_status", __FILE__), *args, "-v", "-", ] if defined? Memory::NO_MEMORY_LEAK_ENVS then envs ||= {} newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } envs = newenvs if newenvs end args.unshift(envs) if envs cmd = [ 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', prepare, 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', '$initial_size = $initial_status.size', code, 'GC.start', ].join("\n") _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) assert(status.success?, FailDesc[status, message, err]) ([:size, (rss && :rss)] & after.members).each do |n| b = before[n] a = after[n] next unless a > 0 and b > 0 assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) end rescue LoadError skip end def assert_cpu_usage_low(msg = nil, pct: 0.01) require 'benchmark' tms = Benchmark.measure(msg || '') { yield } max = pct * tms.real if tms.real < 0.1 # TIME_QUANTUM_USEC in thread_pthread.c warn "test #{msg || 'assert_cpu_usage_low'} too short to be accurate" end # kernel resolution can limit the minimum time we can measure # [ruby-core:81540] min_hz = windows? ? 67 : 100 min_measurable = 1.0 / min_hz min_measurable *= 1.10 # add a little (10%) to account for misc. overheads if max < min_measurable max = min_measurable end assert_operator tms.total, :<=, max, msg end def assert_is_minus_zero(f) assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0") end def assert_file AssertFile end # pattern_list is an array which contains regexp and :*. # :* means any sequence. # # pattern_list is anchored. # Use [:*, regexp, :*] for non-anchored match. def assert_pattern_list(pattern_list, actual, message=nil) rest = actual anchored = true pattern_list.each_with_index {|pattern, i| if pattern == :* anchored = false else if anchored match = /\A#{pattern}/.match(rest) else match = pattern.match(rest) end unless match msg = message(msg) { expect_msg = "Expected #{mu_pp pattern}\n" if /\n[^\n]/ =~ rest actual_mesg = "to match\n" rest.scan(/.*\n+/) { actual_mesg << ' ' << $&.inspect << "+\n" } actual_mesg.sub!(/\+\n\z/, '') else actual_mesg = "to match #{mu_pp rest}" end actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" expect_msg + actual_mesg } assert false, msg end rest = match.post_match anchored = true end } if anchored assert_equal("", rest) end end # threads should respond to shift method. # Array can be used. def assert_join_threads(threads, message = nil) errs = [] values = [] while th = threads.shift begin values << th.value rescue Exception errs << [th, $!] end end if !errs.empty? msg = "exceptions on #{errs.length} threads:\n" + errs.map {|t, err| "#{t.inspect}:\n" + err.backtrace.map.with_index {|line, i| if i == 0 "#{line}: #{err.message} (#{err.class})" else "\tfrom #{line}" end }.join("\n") }.join("\n---\n") if message msg = "#{message}\n#{msg}" end raise MiniTest::Assertion, msg end values end class << (AssertFile = Struct.new(:failure_message).new) include Assertions def assert_file_predicate(predicate, *args) if /\Anot_/ =~ predicate predicate = $' neg = " not" end result = File.__send__(predicate, *args) result = !result if neg mesg = "Expected file ".dup << args.shift.inspect mesg << "#{neg} to be #{predicate}" mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? mesg << " #{failure_message}" if failure_message assert(result, mesg) end alias method_missing assert_file_predicate def for(message) clone.tap {|a| a.failure_message = message} end end class AllFailures attr_reader :failures def initialize @count = 0 @failures = {} end def for(key) @count += 1 yield rescue Exception => e @failures[key] = [@count, e] end def foreach(*keys) keys.each do |key| @count += 1 begin yield key rescue Exception => e @failures[key] = [@count, e] end end end def message i = 0 total = @count.to_s fmt = "%#{total.size}d" @failures.map {|k, (n, v)| "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.message.b.gsub(/^/, ' | ')}" }.join("\n") end def pass? @failures.empty? end end def assert_all_assertions(msg = nil) all = AllFailures.new yield all ensure assert(all.pass?, message(msg) {all.message.chomp(".")}) end alias all_assertions assert_all_assertions def assert_all_assertions_foreach(msg = nil, *keys, &block) all = AllFailures.new all.foreach(*keys, &block) ensure assert(all.pass?, message(msg) {all.message.chomp(".")}) end alias all_assertions_foreach assert_all_assertions_foreach def build_message(head, template=nil, *arguments) #:nodoc: template &&= template.chomp template.gsub(/\G((?:[^\\]|\\.)*?)(\\)?\?/) { $1 + ($2 ? "?" : mu_pp(arguments.shift)) } end def message(msg = nil, *args, &default) # :nodoc: if Proc === msg super(nil, *args) do ary = [msg.call, (default.call if default)].compact.reject(&:empty?) if 1 < ary.length ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? e begin trace = e.backtrace || ['unknown method'] err = ["#{trace.shift}: #{e.message} (#{e.class})"] + trace.map{|t| t.prepend("\t") } _report "bye", Marshal.dump(err.join("\n")) rescue Errno::EPIPE;end exit ensure @stdin.close if @stdin @stdout.close if @stdout end end def _report(res, *args) # :nodoc: @stdout.write(args.empty? ? "#{res}\n" : "#{res} #{args.pack("m0")}\n") end def puke(klass, meth, e) # :nodoc: if e.is_a?(MiniTest::Skip) new_e = MiniTest::Skip.new(e.message) new_e.set_backtrace(e.backtrace) e = new_e end @partial_report << [klass.name, meth, e.is_a?(MiniTest::Assertion) ? e : ProxyError.new(e)] super end def record(suite, method, assertions, time, error) # :nodoc: case error when nil when MiniTest::Assertion, MiniTest::Skip case error.cause when nil, MiniTest::Assertion, MiniTest::Skip else bt = error.backtrace error = error.class.new(error.message) error.set_backtrace(bt) end else error = ProxyError.new(error) end _report "record", Marshal.dump([suite.name, method, assertions, time, error]) super end end end end if $0 == __FILE__ module Test module Unit class TestCase < MiniTest::Unit::TestCase # :nodoc: all undef on_parallel_worker? def on_parallel_worker? true end end end end require 'rubygems' module Gem # :nodoc: end class Gem::TestCase < MiniTest::Unit::TestCase # :nodoc: @@project_dir = File.expand_path('../../../../..', __FILE__) end Test::Unit::Worker.new.run(ARGV) end webrick-1.7.0/test/lib/test/unit/testcase.rb000066400000000000000000000014041376456024300210010ustar00rootroot00000000000000# frozen_string_literal: true require 'test/unit/assertions' module Test module Unit # remove silly TestCase class remove_const(:TestCase) if defined?(self::TestCase) class TestCase < MiniTest::Unit::TestCase # :nodoc: all include Assertions def on_parallel_worker? false end def run runner @options = runner.options super runner end def self.test_order :sorted end def self.method_added(name) super return unless name.to_s.start_with?("test_") @test_methods ||= {} if @test_methods[name] warn "test/unit warning: method #{ self }##{ name } is redefined" end @test_methods[name] = true end end end end webrick-1.7.0/test/webrick/000077500000000000000000000000001376456024300155645ustar00rootroot00000000000000webrick-1.7.0/test/webrick/.htaccess000066400000000000000000000000431376456024300173570ustar00rootroot00000000000000this file should not be published. webrick-1.7.0/test/webrick/test_cgi.rb000066400000000000000000000137701376456024300177220ustar00rootroot00000000000000# coding: US-ASCII # frozen_string_literal: false require_relative "utils" require "webrick" require "test/unit" class TestWEBrickCGI < Test::Unit::TestCase CRLF = "\r\n" def teardown WEBrick::Utils::TimeoutHandler.terminate super end def start_cgi_server(log_tester=TestWEBrick::DefaultLogTester, &block) config = { :CGIInterpreter => TestWEBrick::RubyBin, :DocumentRoot => File.dirname(__FILE__), :DirectoryIndex => ["webrick.cgi"], :RequestCallback => Proc.new{|req, res| def req.meta_vars meta = super meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR) meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV'] return meta end }, } if RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32/ config[:CGIPathEnv] = ENV['PATH'] # runtime dll may not be in system dir. end TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| block.call(server, addr, port, log) } end def test_cgi start_cgi_server{|server, addr, port, log| http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/webrick.cgi") http.request(req){|res| assert_equal("/webrick.cgi", res.body, log.call)} req = Net::HTTP::Get.new("/webrick.cgi/path/info") http.request(req){|res| assert_equal("/path/info", res.body, log.call)} req = Net::HTTP::Get.new("/webrick.cgi/%3F%3F%3F?foo=bar") http.request(req){|res| assert_equal("/???", res.body, log.call)} unless RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32|java/ # Path info of res.body is passed via ENV. # ENV[] returns different value on Windows depending on locale. req = Net::HTTP::Get.new("/webrick.cgi/%A4%DB%A4%B2/%A4%DB%A4%B2") http.request(req){|res| assert_equal("/\xA4\xDB\xA4\xB2/\xA4\xDB\xA4\xB2", res.body, log.call)} end req = Net::HTTP::Get.new("/webrick.cgi?a=1;a=2;b=x") http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} req = Net::HTTP::Get.new("/webrick.cgi?a=1&a=2&b=x") http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} req = Net::HTTP::Post.new("/webrick.cgi?a=x;a=y;b=1") req["Content-Type"] = "application/x-www-form-urlencoded" http.request(req, "a=1;a=2;b=x"){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} req = Net::HTTP::Post.new("/webrick.cgi?a=x&a=y&b=1") req["Content-Type"] = "application/x-www-form-urlencoded" http.request(req, "a=1&a=2&b=x"){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)} req = Net::HTTP::Get.new("/") http.request(req){|res| ary = res.body.lines.to_a assert_match(%r{/$}, ary[0], log.call) assert_match(%r{/webrick.cgi$}, ary[1], log.call) } req = Net::HTTP::Get.new("/webrick.cgi") req["Cookie"] = "CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001" http.request(req){|res| assert_equal( "CUSTOMER=WILE_E_COYOTE\nPART_NUMBER=ROCKET_LAUNCHER_0001\n", res.body, log.call) } req = Net::HTTP::Get.new("/webrick.cgi") cookie = %{$Version="1"; } cookie << %{Customer="WILE_E_COYOTE"; $Path="/acme"; } cookie << %{Part_Number="Rocket_Launcher_0001"; $Path="/acme"; } cookie << %{Shipping="FedEx"; $Path="/acme"} req["Cookie"] = cookie http.request(req){|res| assert_equal("Customer=WILE_E_COYOTE, Shipping=FedEx", res["Set-Cookie"], log.call) assert_equal("Customer=WILE_E_COYOTE\n" + "Part_Number=Rocket_Launcher_0001\n" + "Shipping=FedEx\n", res.body, log.call) } } end def test_bad_request log_tester = lambda {|log, access_log| assert_match(/BadRequest/, log.join) } start_cgi_server(log_tester) {|server, addr, port, log| sock = TCPSocket.new(addr, port) begin sock << "POST /webrick.cgi HTTP/1.0" << CRLF sock << "Content-Type: application/x-www-form-urlencoded" << CRLF sock << "Content-Length: 1024" << CRLF sock << CRLF sock << "a=1&a=2&b=x" sock.close_write assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, sock.read, log.call) ensure sock.close end } end def test_cgi_env start_cgi_server do |server, addr, port, log| http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/webrick.cgi/dumpenv") req['proxy'] = 'http://example.com/' req['hello'] = 'world' http.request(req) do |res| env = Marshal.load(res.body) assert_equal 'world', env['HTTP_HELLO'] assert_not_operator env, :include?, 'HTTP_PROXY' end end end CtrlSeq = [0x7f, *(1..31)].pack("C*").gsub(/\s+/, '') CtrlPat = /#{Regexp.quote(CtrlSeq)}/o DumpPat = /#{Regexp.quote(CtrlSeq.dump[1...-1])}/o def test_bad_uri log_tester = lambda {|log, access_log| assert_equal(1, log.length) assert_match(/ERROR bad URI/, log[0]) } start_cgi_server(log_tester) {|server, addr, port, log| res = TCPSocket.open(addr, port) {|sock| sock << "GET /#{CtrlSeq}#{CRLF}#{CRLF}" sock.close_write sock.read } assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res) s = log.call.each_line.grep(/ERROR bad URI/)[0] assert_match(DumpPat, s) assert_not_match(CtrlPat, s) } end def test_bad_header log_tester = lambda {|log, access_log| assert_equal(1, log.length) assert_match(/ERROR bad header/, log[0]) } start_cgi_server(log_tester) {|server, addr, port, log| res = TCPSocket.open(addr, port) {|sock| sock << "GET / HTTP/1.0#{CRLF}#{CtrlSeq}#{CRLF}#{CRLF}" sock.close_write sock.read } assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res) s = log.call.each_line.grep(/ERROR bad header/)[0] assert_match(DumpPat, s) assert_not_match(CtrlPat, s) } end end webrick-1.7.0/test/webrick/test_config.rb000066400000000000000000000010111376456024300204060ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick/config" class TestWEBrickConfig < Test::Unit::TestCase def test_server_name_default config = WEBrick::Config::General.dup assert_equal(false, config.key?(:ServerName)) assert_equal(WEBrick::Utils.getservername, config[:ServerName]) assert_equal(true, config.key?(:ServerName)) end def test_server_name_set_nil config = WEBrick::Config::General.dup.update(ServerName: nil) assert_equal(nil, config[:ServerName]) end end webrick-1.7.0/test/webrick/test_cookie.rb000066400000000000000000000116521376456024300204260ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick/cookie" class TestWEBrickCookie < Test::Unit::TestCase def test_new cookie = WEBrick::Cookie.new("foo","bar") assert_equal("foo", cookie.name) assert_equal("bar", cookie.value) assert_equal("foo=bar", cookie.to_s) end def test_time cookie = WEBrick::Cookie.new("foo","bar") t = 1000000000 cookie.max_age = t assert_match(t.to_s, cookie.to_s) cookie = WEBrick::Cookie.new("foo","bar") t = Time.at(1000000000) cookie.expires = t assert_equal(Time, cookie.expires.class) assert_equal(t, cookie.expires) ts = t.httpdate cookie.expires = ts assert_equal(Time, cookie.expires.class) assert_equal(t, cookie.expires) assert_match(ts, cookie.to_s) end def test_parse data = "" data << '$Version="1"; ' data << 'Customer="WILE_E_COYOTE"; $Path="/acme"; ' data << 'Part_Number="Rocket_Launcher_0001"; $Path="/acme"; ' data << 'Shipping="FedEx"; $Path="/acme"' cookies = WEBrick::Cookie.parse(data) assert_equal(3, cookies.size) assert_equal(1, cookies[0].version) assert_equal("Customer", cookies[0].name) assert_equal("WILE_E_COYOTE", cookies[0].value) assert_equal("/acme", cookies[0].path) assert_equal(1, cookies[1].version) assert_equal("Part_Number", cookies[1].name) assert_equal("Rocket_Launcher_0001", cookies[1].value) assert_equal(1, cookies[2].version) assert_equal("Shipping", cookies[2].name) assert_equal("FedEx", cookies[2].value) data = "hoge=moge; __div__session=9865ecfd514be7f7" cookies = WEBrick::Cookie.parse(data) assert_equal(2, cookies.size) assert_equal(0, cookies[0].version) assert_equal("hoge", cookies[0].name) assert_equal("moge", cookies[0].value) assert_equal("__div__session", cookies[1].name) assert_equal("9865ecfd514be7f7", cookies[1].value) # don't allow ,-separator data = "hoge=moge, __div__session=9865ecfd514be7f7" cookies = WEBrick::Cookie.parse(data) assert_equal(1, cookies.size) assert_equal(0, cookies[0].version) assert_equal("hoge", cookies[0].name) assert_equal("moge, __div__session=9865ecfd514be7f7", cookies[0].value) end def test_parse_no_whitespace data = [ '$Version="1"; ', 'Customer="WILE_E_COYOTE";$Path="/acme";', # no SP between cookie-string 'Part_Number="Rocket_Launcher_0001";$Path="/acme";', # no SP between cookie-string 'Shipping="FedEx";$Path="/acme"' ].join cookies = WEBrick::Cookie.parse(data) assert_equal(1, cookies.size) end def test_parse_too_much_whitespaces # According to RFC6265, # cookie-string = cookie-pair *( ";" SP cookie-pair ) # So single 0x20 is needed after ';'. We allow multiple spaces here for # compatibility with older WEBrick versions. data = [ '$Version="1"; ', 'Customer="WILE_E_COYOTE";$Path="/acme"; ', # no SP between cookie-string 'Part_Number="Rocket_Launcher_0001";$Path="/acme"; ', # no SP between cookie-string 'Shipping="FedEx";$Path="/acme"' ].join cookies = WEBrick::Cookie.parse(data) assert_equal(3, cookies.size) end def test_parse_set_cookie data = %(Customer="WILE_E_COYOTE"; Version="1"; Path="/acme") cookie = WEBrick::Cookie.parse_set_cookie(data) assert_equal("Customer", cookie.name) assert_equal("WILE_E_COYOTE", cookie.value) assert_equal(1, cookie.version) assert_equal("/acme", cookie.path) data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure) cookie = WEBrick::Cookie.parse_set_cookie(data) assert_equal("Shipping", cookie.name) assert_equal("FedEx", cookie.value) assert_equal(1, cookie.version) assert_equal("/acme", cookie.path) assert_equal(true, cookie.secure) end def test_parse_set_cookies data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure) data << %(, CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT; path=/; Secure) data << %(, name="Aaron"; Version="1"; path="/acme") cookies = WEBrick::Cookie.parse_set_cookies(data) assert_equal(3, cookies.length) fed_ex = cookies.find { |c| c.name == 'Shipping' } assert_not_nil(fed_ex) assert_equal("Shipping", fed_ex.name) assert_equal("FedEx", fed_ex.value) assert_equal(1, fed_ex.version) assert_equal("/acme", fed_ex.path) assert_equal(true, fed_ex.secure) name = cookies.find { |c| c.name == 'name' } assert_not_nil(name) assert_equal("name", name.name) assert_equal("Aaron", name.value) assert_equal(1, name.version) assert_equal("/acme", name.path) customer = cookies.find { |c| c.name == 'CUSTOMER' } assert_not_nil(customer) assert_equal("CUSTOMER", customer.name) assert_equal("WILE_E_COYOTE", customer.value) assert_equal(0, customer.version) assert_equal("/", customer.path) assert_equal(Time.utc(1999, 11, 9, 23, 12, 40), customer.expires) end end webrick-1.7.0/test/webrick/test_do_not_reverse_lookup.rb000066400000000000000000000052061376456024300235610ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick" require_relative "utils" class TestDoNotReverseLookup < Test::Unit::TestCase class DNRL < WEBrick::GenericServer def run(sock) sock << sock.do_not_reverse_lookup.to_s end end @@original_do_not_reverse_lookup_value = Socket.do_not_reverse_lookup def teardown Socket.do_not_reverse_lookup = @@original_do_not_reverse_lookup_value end def do_not_reverse_lookup?(config) result = nil TestWEBrick.start_server(DNRL, config) do |server, addr, port, log| TCPSocket.open(addr, port) do |sock| result = {'true' => true, 'false' => false}[sock.gets] end end result end # +--------------------------------------------------------------------------+ # | Expected interaction between Socket.do_not_reverse_lookup | # | and WEBrick::Config::General[:DoNotReverseLookup] | # +----------------------------+---------------------------------------------+ # | |WEBrick::Config::General[:DoNotReverseLookup]| # +----------------------------+--------------+---------------+--------------+ # |Socket.do_not_reverse_lookup| TRUE | FALSE | NIL | # +----------------------------+--------------+---------------+--------------+ # | TRUE | true | false | true | # +----------------------------+--------------+---------------+--------------+ # | FALSE | true | false | false | # +----------------------------+--------------+---------------+--------------+ def test_socket_dnrl_true_server_dnrl_true Socket.do_not_reverse_lookup = true assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true)) end def test_socket_dnrl_true_server_dnrl_false Socket.do_not_reverse_lookup = true assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false)) end def test_socket_dnrl_true_server_dnrl_nil Socket.do_not_reverse_lookup = true assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => nil)) end def test_socket_dnrl_false_server_dnrl_true Socket.do_not_reverse_lookup = false assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true)) end def test_socket_dnrl_false_server_dnrl_false Socket.do_not_reverse_lookup = false assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false)) end def test_socket_dnrl_false_server_dnrl_nil Socket.do_not_reverse_lookup = false assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => nil)) end end webrick-1.7.0/test/webrick/test_filehandler.rb000066400000000000000000000343101376456024300214260ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require_relative "utils.rb" require "webrick" require "stringio" require "tmpdir" class WEBrick::TestFileHandler < Test::Unit::TestCase def teardown WEBrick::Utils::TimeoutHandler.terminate super end def default_file_handler(filename) klass = WEBrick::HTTPServlet::DefaultFileHandler klass.new(WEBrick::Config::HTTP, filename) end def windows? File.directory?("\\") end def get_res_body(res) sio = StringIO.new sio.binmode res.send_body(sio) sio.string end def make_range_request(range_spec) msg = <<-END_OF_REQUEST GET / HTTP/1.0 Range: #{range_spec} END_OF_REQUEST return StringIO.new(msg.gsub(/^ {6}/, "")) end def make_range_response(file, range_spec) req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(make_range_request(range_spec)) res = WEBrick::HTTPResponse.new(WEBrick::Config::HTTP) size = File.size(file) handler = default_file_handler(file) handler.make_partial_content(req, res, file, size) return res end def test_make_partial_content filename = __FILE__ filesize = File.size(filename) res = make_range_response(filename, "bytes=#{filesize-100}-") assert_match(%r{^text/plain}, res["content-type"]) assert_equal(100, get_res_body(res).size) res = make_range_response(filename, "bytes=-100") assert_match(%r{^text/plain}, res["content-type"]) assert_equal(100, get_res_body(res).size) res = make_range_response(filename, "bytes=0-99") assert_match(%r{^text/plain}, res["content-type"]) assert_equal(100, get_res_body(res).size) res = make_range_response(filename, "bytes=100-199") assert_match(%r{^text/plain}, res["content-type"]) assert_equal(100, get_res_body(res).size) res = make_range_response(filename, "bytes=0-0") assert_match(%r{^text/plain}, res["content-type"]) assert_equal(1, get_res_body(res).size) res = make_range_response(filename, "bytes=-1") assert_match(%r{^text/plain}, res["content-type"]) assert_equal(1, get_res_body(res).size) res = make_range_response(filename, "bytes=0-0, -2") assert_match(%r{^multipart/byteranges}, res["content-type"]) body = get_res_body(res) boundary = /; boundary=(.+)/.match(res['content-type'])[1] off = filesize - 2 last = filesize - 1 exp = "--#{boundary}\r\n" \ "Content-Type: text/plain\r\n" \ "Content-Range: bytes 0-0/#{filesize}\r\n" \ "\r\n" \ "#{IO.read(__FILE__, 1)}\r\n" \ "--#{boundary}\r\n" \ "Content-Type: text/plain\r\n" \ "Content-Range: bytes #{off}-#{last}/#{filesize}\r\n" \ "\r\n" \ "#{IO.read(__FILE__, 2, off)}\r\n" \ "--#{boundary}--\r\n" assert_equal exp, body end def test_filehandler config = { :DocumentRoot => File.dirname(__FILE__), } this_file = File.basename(__FILE__) filesize = File.size(__FILE__) this_data = File.binread(__FILE__) range = nil bug2593 = '[ruby-dev:40030]' TestWEBrick.start_httpserver(config) do |server, addr, port, log| begin server[:DocumentRootOptions][:NondisclosureName] = [] http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/") http.request(req){|res| assert_equal("200", res.code, log.call) assert_equal("text/html", res.content_type, log.call) assert_match(/HREF="#{this_file}"/, res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}") http.request(req){|res| assert_equal("200", res.code, log.call) assert_equal("text/plain", res.content_type, log.call) assert_equal(this_data, res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=#{filesize-100}-") http.request(req){|res| assert_equal("206", res.code, log.call) assert_equal("text/plain", res.content_type, log.call) assert_nothing_raised(bug2593) {range = res.content_range} assert_equal((filesize-100)..(filesize-1), range, log.call) assert_equal(this_data[-100..-1], res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=-100") http.request(req){|res| assert_equal("206", res.code, log.call) assert_equal("text/plain", res.content_type, log.call) assert_nothing_raised(bug2593) {range = res.content_range} assert_equal((filesize-100)..(filesize-1), range, log.call) assert_equal(this_data[-100..-1], res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-99") http.request(req){|res| assert_equal("206", res.code, log.call) assert_equal("text/plain", res.content_type, log.call) assert_nothing_raised(bug2593) {range = res.content_range} assert_equal(0..99, range, log.call) assert_equal(this_data[0..99], res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=100-199") http.request(req){|res| assert_equal("206", res.code, log.call) assert_equal("text/plain", res.content_type, log.call) assert_nothing_raised(bug2593) {range = res.content_range} assert_equal(100..199, range, log.call) assert_equal(this_data[100..199], res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-0") http.request(req){|res| assert_equal("206", res.code, log.call) assert_equal("text/plain", res.content_type, log.call) assert_nothing_raised(bug2593) {range = res.content_range} assert_equal(0..0, range, log.call) assert_equal(this_data[0..0], res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=-1") http.request(req){|res| assert_equal("206", res.code, log.call) assert_equal("text/plain", res.content_type, log.call) assert_nothing_raised(bug2593) {range = res.content_range} assert_equal((filesize-1)..(filesize-1), range, log.call) assert_equal(this_data[-1, 1], res.body, log.call) } req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-0, -2") http.request(req){|res| assert_equal("206", res.code, log.call) assert_equal("multipart/byteranges", res.content_type, log.call) } ensure server[:DocumentRootOptions].delete :NondisclosureName end end end def test_non_disclosure_name config = { :DocumentRoot => File.dirname(__FILE__), } log_tester = lambda {|log, access_log| log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } log = log.reject {|s| /WARN the request refers nondisclosure name/ =~ s } assert_equal([], log) } this_file = File.basename(__FILE__) TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| http = Net::HTTP.new(addr, port) doc_root_opts = server[:DocumentRootOptions] doc_root_opts[:NondisclosureName] = %w(.ht* *~ test_*) req = Net::HTTP::Get.new("/") http.request(req){|res| assert_equal("200", res.code, log.call) assert_equal("text/html", res.content_type, log.call) assert_no_match(/HREF="#{File.basename(__FILE__)}"/, res.body) } req = Net::HTTP::Get.new("/#{this_file}") http.request(req){|res| assert_equal("404", res.code, log.call) } doc_root_opts[:NondisclosureName] = %w(.ht* *~ TEST_*) http.request(req){|res| assert_equal("404", res.code, log.call) } end end def test_directory_traversal return if File.executable?(__FILE__) # skip on strange file system config = { :DocumentRoot => File.dirname(__FILE__), } log_tester = lambda {|log, access_log| log = log.reject {|s| /ERROR bad URI/ =~ s } log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } assert_equal([], log) } TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/../../") http.request(req){|res| assert_equal("400", res.code, log.call) } req = Net::HTTP::Get.new("/..%5c../#{File.basename(__FILE__)}") http.request(req){|res| assert_equal(windows? ? "200" : "404", res.code, log.call) } req = Net::HTTP::Get.new("/..%5c..%5cruby.c") http.request(req){|res| assert_equal("404", res.code, log.call) } end end def test_unwise_in_path if windows? config = { :DocumentRoot => File.dirname(__FILE__), } TestWEBrick.start_httpserver(config) do |server, addr, port, log| http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/..%5c..") http.request(req){|res| assert_equal("301", res.code, log.call) } end end end def test_short_filename return if File.executable?(__FILE__) # skip on strange file system config = { :CGIInterpreter => TestWEBrick::RubyBin, :DocumentRoot => File.dirname(__FILE__), :CGIPathEnv => ENV['PATH'], } log_tester = lambda {|log, access_log| log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } log = log.reject {|s| /WARN the request refers nondisclosure name/ =~ s } assert_equal([], log) } TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| http = Net::HTTP.new(addr, port) if windows? root = config[:DocumentRoot].tr("/", "\\") fname = IO.popen(%W[dir /x #{root}\\webrick_long_filename.cgi], encoding: "binary", &:read) fname.sub!(/\A.*$^$.*$^$/m, '') if fname fname = fname[/\s(w.+?cgi)\s/i, 1] fname.downcase! end else fname = "webric~1.cgi" end req = Net::HTTP::Get.new("/#{fname}/test") http.request(req) do |res| if windows? assert_equal("200", res.code, log.call) assert_equal("/test", res.body, log.call) else assert_equal("404", res.code, log.call) end end req = Net::HTTP::Get.new("/.htaccess") http.request(req) {|res| assert_equal("404", res.code, log.call) } req = Net::HTTP::Get.new("/htacce~1") http.request(req) {|res| assert_equal("404", res.code, log.call) } req = Net::HTTP::Get.new("/HTACCE~1") http.request(req) {|res| assert_equal("404", res.code, log.call) } end end def test_multibyte_char_in_path if Encoding.default_external == Encoding.find('US-ASCII') reset_encoding = true verb = $VERBOSE $VERBOSE = false Encoding.default_external = Encoding.find('UTF-8') end c = "\u00a7" begin c = c.encode('filesystem') rescue EncodingError c = c.b end Dir.mktmpdir(c) do |dir| basename = "#{c}.txt" File.write("#{dir}/#{basename}", "test_multibyte_char_in_path") Dir.mkdir("#{dir}/#{c}") File.write("#{dir}/#{c}/#{basename}", "nested") config = { :DocumentRoot => dir, :DirectoryIndex => [basename], } TestWEBrick.start_httpserver(config) do |server, addr, port, log| http = Net::HTTP.new(addr, port) path = "/#{basename}" req = Net::HTTP::Get.new(WEBrick::HTTPUtils::escape(path)) http.request(req){|res| assert_equal("200", res.code, log.call + "\nFilesystem encoding is #{Encoding.find('filesystem')}") } path = "/#{c}/#{basename}" req = Net::HTTP::Get.new(WEBrick::HTTPUtils::escape(path)) http.request(req){|res| assert_equal("200", res.code, log.call) } req = Net::HTTP::Get.new('/') http.request(req){|res| assert_equal("test_multibyte_char_in_path", res.body, log.call) } end end ensure if reset_encoding Encoding.default_external = Encoding.find('US-ASCII') $VERBOSE = verb end end def test_script_disclosure return if File.executable?(__FILE__) # skip on strange file system config = { :CGIInterpreter => TestWEBrick::RubyBinArray, :DocumentRoot => File.dirname(__FILE__), :CGIPathEnv => ENV['PATH'], :RequestCallback => Proc.new{|req, res| def req.meta_vars meta = super meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR) meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV'] return meta end }, } log_tester = lambda {|log, access_log| log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } assert_equal([], log) } TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| http = Net::HTTP.new(addr, port) http.read_timeout = EnvUtil.apply_timeout_scale(60) http.write_timeout = EnvUtil.apply_timeout_scale(60) if http.respond_to?(:write_timeout=) req = Net::HTTP::Get.new("/webrick.cgi/test") http.request(req) do |res| assert_equal("200", res.code, log.call) assert_equal("/test", res.body, log.call) end resok = windows? response_assertion = Proc.new do |res| if resok assert_equal("200", res.code, log.call) assert_equal("/test", res.body, log.call) else assert_equal("404", res.code, log.call) end end req = Net::HTTP::Get.new("/webrick.cgi%20/test") http.request(req, &response_assertion) req = Net::HTTP::Get.new("/webrick.cgi./test") http.request(req, &response_assertion) resok &&= File.exist?(__FILE__+"::$DATA") req = Net::HTTP::Get.new("/webrick.cgi::$DATA/test") http.request(req, &response_assertion) end end def test_erbhandler config = { :DocumentRoot => File.dirname(__FILE__) } log_tester = lambda {|log, access_log| log = log.reject {|s| /ERROR `.*\' not found\./ =~ s } assert_equal([], log) } TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log| http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/webrick.rhtml") http.request(req) do |res| assert_equal("200", res.code, log.call) assert_match %r!\Areq to http://[^/]+/webrick\.rhtml {}\n!, res.body end end end end webrick-1.7.0/test/webrick/test_htgroup.rb000066400000000000000000000011411376456024300206350ustar00rootroot00000000000000require "tempfile" require "test/unit" require "webrick/httpauth/htgroup" class TestHtgroup < Test::Unit::TestCase def test_htgroup Tempfile.create('test_htgroup') do |tmpfile| tmpfile.close tmp_group = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path) tmp_group.add 'superheroes', %w[spiderman batman] tmp_group.add 'supervillains', %w[joker] tmp_group.flush htgroup = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path) assert_equal(htgroup.members('superheroes'), %w[spiderman batman]) assert_equal(htgroup.members('supervillains'), %w[joker]) end end end webrick-1.7.0/test/webrick/test_htmlutils.rb000066400000000000000000000014211376456024300211730ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick/htmlutils" class TestWEBrickHTMLUtils < Test::Unit::TestCase include WEBrick::HTMLUtils def test_escape assert_equal("foo", escape("foo")) assert_equal("foo bar", escape("foo bar")) assert_equal("foo&bar", escape("foo&bar")) assert_equal("foo"bar", escape("foo\"bar")) assert_equal("foo>bar", escape("foo>bar")) assert_equal("foo<bar", escape("foo realm, :UserDB => htpasswd, :Logger => server.logger ) auth.authenticate(req, res) res.body = "hoge" } http = Net::HTTP.new(addr, port) g = Net::HTTP::Get.new(path) g.basic_auth("webrick", "supersecretpassword") http.request(g){|res| assert_equal("hoge", res.body, log.call)} g.basic_auth("webrick", "not super") http.request(g){|res| assert_not_equal("hoge", res.body, log.call)} } } end define_method(:"test_basic_auth_bad_username_htpasswd_#{hash_algo}") do log_tester = lambda {|log, access_log| assert_equal(2, log.length) assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed\./, log[0]) assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1]) } TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| realm = "WEBrick's realm" path = "/basic_auth" Tempfile.create("test_webrick_auth") {|tmpfile| tmpfile.close tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo) tmp_pass.set_passwd(realm, "webrick", "supersecretpassword") tmp_pass.set_passwd(realm, "foo", "supersecretpassword") tmp_pass.flush htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo) users = [] htpasswd.each{|user, pass| users << user } server.mount_proc(path){|req, res| auth = WEBrick::HTTPAuth::BasicAuth.new( :Realm => realm, :UserDB => htpasswd, :Logger => server.logger ) auth.authenticate(req, res) res.body = "hoge" } http = Net::HTTP.new(addr, port) g = Net::HTTP::Get.new(path) g.basic_auth("foo\ebar", "passwd") http.request(g){|res| assert_not_equal("hoge", res.body, log.call) } } } end end DIGESTRES_ = / ([a-zA-Z\-]+) [ \t]*(?:\r\n[ \t]*)* = [ \t]*(?:\r\n[ \t]*)* (?: "((?:[^"]+|\\[\x00-\x7F])*)" | ([!\#$%&'*+\-.0-9A-Z^_`a-z|~]+) )/x def test_digest_auth log_tester = lambda {|log, access_log| log.reject! {|line| /\A\s*\z/ =~ line } pats = [ /ERROR Digest WEBrick's realm: no credentials in the request\./, /ERROR WEBrick::HTTPStatus::Unauthorized/, /ERROR Digest WEBrick's realm: webrick: digest unmatch\./ ] pats.each {|pat| assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}") log.reject! {|line| pat =~ line } } assert_equal([], log) } TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| realm = "WEBrick's realm" path = "/digest_auth" Tempfile.create("test_webrick_auth") {|tmpfile| tmpfile.close tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) tmp_pass.set_passwd(realm, "webrick", "supersecretpassword") tmp_pass.set_passwd(realm, "foo", "supersecretpassword") tmp_pass.flush htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) users = [] htdigest.each{|user, pass| users << user } assert_equal(2, users.size, log.call) assert(users.member?("webrick"), log.call) assert(users.member?("foo"), log.call) auth = WEBrick::HTTPAuth::DigestAuth.new( :Realm => realm, :UserDB => htdigest, :Algorithm => 'MD5', :Logger => server.logger ) server.mount_proc(path){|req, res| auth.authenticate(req, res) res.body = "hoge" } Net::HTTP.start(addr, port) do |http| g = Net::HTTP::Get.new(path) params = {} http.request(g) do |res| assert_equal('401', res.code, log.call) res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token| params[key.downcase] = token || quoted.delete('\\') end params['uri'] = "http://#{addr}:#{port}#{path}" end g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params) http.request(g){|res| assert_equal("hoge", res.body, log.call)} params['algorithm'].downcase! #4936 g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params) http.request(g){|res| assert_equal("hoge", res.body, log.call)} g['Authorization'] = credentials_for_request('webrick', "not super", params) http.request(g){|res| assert_not_equal("hoge", res.body, log.call)} end } } end def test_digest_auth_int log_tester = lambda {|log, access_log| log.reject! {|line| /\A\s*\z/ =~ line } pats = [ /ERROR Digest wb auth-int realm: no credentials in the request\./, /ERROR WEBrick::HTTPStatus::Unauthorized/, /ERROR Digest wb auth-int realm: foo: digest unmatch\./ ] pats.each {|pat| assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}") log.reject! {|line| pat =~ line } } assert_equal([], log) } TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log| realm = "wb auth-int realm" path = "/digest_auth_int" Tempfile.create("test_webrick_auth_int") {|tmpfile| tmpfile.close tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) tmp_pass.set_passwd(realm, "foo", "Hunter2") tmp_pass.flush htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path) users = [] htdigest.each{|user, pass| users << user } assert_equal %w(foo), users auth = WEBrick::HTTPAuth::DigestAuth.new( :Realm => realm, :UserDB => htdigest, :Algorithm => 'MD5', :Logger => server.logger, :Qop => %w(auth-int), ) server.mount_proc(path){|req, res| auth.authenticate(req, res) res.body = "bbb" } Net::HTTP.start(addr, port) do |http| post = Net::HTTP::Post.new(path) params = {} data = 'hello=world' body = StringIO.new(data) post.content_length = data.bytesize post['Content-Type'] = 'application/x-www-form-urlencoded' post.body_stream = body http.request(post) do |res| assert_equal('401', res.code, log.call) res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token| params[key.downcase] = token || quoted.delete('\\') end params['uri'] = "http://#{addr}:#{port}#{path}" end body.rewind cred = credentials_for_request('foo', 'Hunter3', params, body) post['Authorization'] = cred post.body_stream = body http.request(post){|res| assert_equal('401', res.code, log.call) assert_not_equal("bbb", res.body, log.call) } body.rewind cred = credentials_for_request('foo', 'Hunter2', params, body) post['Authorization'] = cred post.body_stream = body http.request(post){|res| assert_equal("bbb", res.body, log.call)} end } } end def test_digest_auth_invalid digest_auth = WEBrick::HTTPAuth::DigestAuth.new(Realm: 'realm', UserDB: '') def digest_auth.error(fmt, *) end def digest_auth.try_bad_request(len) request = {"Authorization" => %[Digest a="#{'\b'*len}]} authenticate request, nil end bad_request = WEBrick::HTTPStatus::BadRequest t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) assert_raise(bad_request) {digest_auth.try_bad_request(10)} limit = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) [20, 50, 100, 200].each do |len| assert_raise(bad_request) do Timeout.timeout(len*limit) {digest_auth.try_bad_request(len)} end end end private def credentials_for_request(user, password, params, body = nil) cnonce = "hoge" nonce_count = 1 ha1 = "#{user}:#{params['realm']}:#{password}" if body dig = Digest::MD5.new while buf = body.read(16384) dig.update(buf) end body.rewind ha2 = "POST:#{params['uri']}:#{dig.hexdigest}" else ha2 = "GET:#{params['uri']}" end request_digest = "#{Digest::MD5.hexdigest(ha1)}:" \ "#{params['nonce']}:#{'%08x' % nonce_count}:#{cnonce}:#{params['qop']}:" \ "#{Digest::MD5.hexdigest(ha2)}" "Digest username=\"#{user}\"" \ ", realm=\"#{params['realm']}\"" \ ", nonce=\"#{params['nonce']}\"" \ ", uri=\"#{params['uri']}\"" \ ", qop=#{params['qop']}" \ ", nc=#{'%08x' % nonce_count}" \ ", cnonce=\"#{cnonce}\"" \ ", response=\"#{Digest::MD5.hexdigest(request_digest)}\"" \ ", opaque=\"#{params['opaque']}\"" \ ", algorithm=#{params['algorithm']}" end end webrick-1.7.0/test/webrick/test_httpproxy.rb000066400000000000000000000431451376456024300212400ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "net/http" require "webrick" require "webrick/httpproxy" begin require "webrick/ssl" require "net/https" rescue LoadError # test_connect will be skipped end require File.expand_path("utils.rb", File.dirname(__FILE__)) class TestWEBrickHTTPProxy < Test::Unit::TestCase def teardown WEBrick::Utils::TimeoutHandler.terminate super end def test_fake_proxy assert_nil(WEBrick::FakeProxyURI.scheme) assert_nil(WEBrick::FakeProxyURI.host) assert_nil(WEBrick::FakeProxyURI.port) assert_nil(WEBrick::FakeProxyURI.path) assert_nil(WEBrick::FakeProxyURI.userinfo) assert_raise(NoMethodError){ WEBrick::FakeProxyURI.foo } end def test_proxy # Testing GET or POST to the proxy server # Note that the proxy server works as the origin server. # +------+ # V | # client -------> proxy ---+ # GET / POST GET / POST # proxy_handler_called = request_handler_called = 0 config = { :ServerName => "localhost.localdomain", :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 }, :RequestCallback => Proc.new{|req, res| request_handler_called += 1 } } TestWEBrick.start_httpproxy(config){|server, addr, port, log| server.mount_proc("/"){|req, res| res.body = "#{req.request_method} #{req.path} #{req.body}" } http = Net::HTTP.new(addr, port, addr, port) req = Net::HTTP::Get.new("/") http.request(req){|res| assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call) assert_equal("GET / ", res.body, log.call) } assert_equal(1, proxy_handler_called, log.call) assert_equal(2, request_handler_called, log.call) req = Net::HTTP::Head.new("/") http.request(req){|res| assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call) assert_nil(res.body, log.call) } assert_equal(2, proxy_handler_called, log.call) assert_equal(4, request_handler_called, log.call) req = Net::HTTP::Post.new("/") req.body = "post-data" req.content_type = "application/x-www-form-urlencoded" http.request(req){|res| assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call) assert_equal("POST / post-data", res.body, log.call) } assert_equal(3, proxy_handler_called, log.call) assert_equal(6, request_handler_called, log.call) } end def test_no_proxy # Testing GET or POST to the proxy server without proxy request. # # client -------> proxy # GET / POST # proxy_handler_called = request_handler_called = 0 config = { :ServerName => "localhost.localdomain", :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 }, :RequestCallback => Proc.new{|req, res| request_handler_called += 1 } } TestWEBrick.start_httpproxy(config){|server, addr, port, log| server.mount_proc("/"){|req, res| res.body = "#{req.request_method} #{req.path} #{req.body}" } http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/") http.request(req){|res| assert_nil(res["via"], log.call) assert_equal("GET / ", res.body, log.call) } assert_equal(0, proxy_handler_called, log.call) assert_equal(1, request_handler_called, log.call) req = Net::HTTP::Head.new("/") http.request(req){|res| assert_nil(res["via"], log.call) assert_nil(res.body, log.call) } assert_equal(0, proxy_handler_called, log.call) assert_equal(2, request_handler_called, log.call) req = Net::HTTP::Post.new("/") req.content_type = "application/x-www-form-urlencoded" req.body = "post-data" http.request(req){|res| assert_nil(res["via"], log.call) assert_equal("POST / post-data", res.body, log.call) } assert_equal(0, proxy_handler_called, log.call) assert_equal(3, request_handler_called, log.call) } end def test_big_bodies require 'digest/md5' rand_str = File.read(__FILE__) rand_str.freeze nr = 1024 ** 2 / rand_str.size # bigger works, too exp = Digest::MD5.new nr.times { exp.update(rand_str) } exp = exp.hexdigest TestWEBrick.start_httpserver do |o_server, o_addr, o_port, o_log| o_server.mount_proc('/') do |req, res| case req.request_method when 'GET' res['content-type'] = 'application/octet-stream' if req.path == '/length' res['content-length'] = (nr * rand_str.size).to_s else res.chunked = true end res.body = ->(socket) { nr.times { socket.write(rand_str) } } when 'POST' dig = Digest::MD5.new req.body { |buf| dig.update(buf); buf.clear } res['content-type'] = 'text/plain' res['content-length'] = '32' res.body = dig.hexdigest end end http = Net::HTTP.new(o_addr, o_port) IO.pipe do |rd, wr| headers = { 'Content-Type' => 'application/octet-stream', 'Transfer-Encoding' => 'chunked', } post = Net::HTTP::Post.new('/', headers) th = Thread.new { nr.times { wr.write(rand_str) }; wr.close } post.body_stream = rd http.request(post) do |res| assert_equal 'text/plain', res['content-type'] assert_equal 32, res.content_length assert_equal exp, res.body end assert_nil th.value end TestWEBrick.start_httpproxy do |p_server, p_addr, p_port, p_log| http = Net::HTTP.new(o_addr, o_port, p_addr, p_port) http.request_get('/length') do |res| assert_equal(nr * rand_str.size, res.content_length) dig = Digest::MD5.new res.read_body { |buf| dig.update(buf); buf.clear } assert_equal exp, dig.hexdigest end http.request_get('/') do |res| assert_predicate res, :chunked? dig = Digest::MD5.new res.read_body { |buf| dig.update(buf); buf.clear } assert_equal exp, dig.hexdigest end IO.pipe do |rd, wr| headers = { 'Content-Type' => 'application/octet-stream', 'Content-Length' => (nr * rand_str.size).to_s, } post = Net::HTTP::Post.new('/', headers) th = Thread.new { nr.times { wr.write(rand_str) }; wr.close } post.body_stream = rd http.request(post) do |res| assert_equal 'text/plain', res['content-type'] assert_equal 32, res.content_length assert_equal exp, res.body end assert_nil th.value end IO.pipe do |rd, wr| headers = { 'Content-Type' => 'application/octet-stream', 'Transfer-Encoding' => 'chunked', } post = Net::HTTP::Post.new('/', headers) th = Thread.new { nr.times { wr.write(rand_str) }; wr.close } post.body_stream = rd http.request(post) do |res| assert_equal 'text/plain', res['content-type'] assert_equal 32, res.content_length assert_equal exp, res.body end assert_nil th.value end end end end if RUBY_VERSION >= '2.5' def test_http10_proxy_chunked # Testing HTTP/1.0 client request and HTTP/1.1 chunked response # from origin server. # +------+ # V | # client -------> proxy ---+ # GET GET # HTTP/1.0 HTTP/1.1 # non-chunked chunked # proxy_handler_called = request_handler_called = 0 config = { :ServerName => "localhost.localdomain", :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 }, :RequestCallback => Proc.new{|req, res| request_handler_called += 1 } } log_tester = lambda {|log, access_log| log.reject! {|str| %r{WARN chunked is set for an HTTP/1\.0 request\. \(ignored\)} =~ str } assert_equal([], log) } TestWEBrick.start_httpproxy(config, log_tester){|server, addr, port, log| body = nil server.mount_proc("/"){|req, res| body = "#{req.request_method} #{req.path} #{req.body}" res.chunked = true res.body = -> (socket) { body.each_char {|c| socket.write c } } } # Don't use Net::HTTP because it uses HTTP/1.1. TCPSocket.open(addr, port) {|s| s.write "GET / HTTP/1.0\r\nHost: localhost.localdomain\r\n\r\n" response = s.read assert_equal(body, response[/.*\z/]) } } end def make_certificate(key, cn) subject = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=#{cn}") exts = [ ["keyUsage", "keyEncipherment,digitalSignature", true], ] cert = OpenSSL::X509::Certificate.new cert.version = 2 cert.serial = 1 cert.subject = subject cert.issuer = subject cert.public_key = key cert.not_before = Time.now - 3600 cert.not_after = Time.now + 3600 ef = OpenSSL::X509::ExtensionFactory.new(cert, cert) exts.each {|args| cert.add_extension(ef.create_extension(*args)) } cert.sign(key, "sha256") return cert end if defined?(OpenSSL::SSL) def test_connect # Testing CONNECT to proxy server # # client -----------> proxy -----------> https # 1. CONNECT establish TCP # 2. ---- establish SSL session ---> # 3. ------- GET or POST ----------> # key = TEST_KEY_RSA2048 cert = make_certificate(key, "127.0.0.1") s_config = { :SSLEnable =>true, :ServerName => "localhost", :SSLCertificate => cert, :SSLPrivateKey => key, } config = { :ServerName => "localhost.localdomain", :RequestCallback => Proc.new{|req, res| assert_equal("CONNECT", req.request_method) }, } TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port, s_log| s_server.mount_proc("/"){|req, res| res.body = "SSL #{req.request_method} #{req.path} #{req.body}" } TestWEBrick.start_httpproxy(config){|server, addr, port, log| http = Net::HTTP.new("127.0.0.1", s_port, addr, port) http.use_ssl = true http.verify_callback = Proc.new do |preverify_ok, store_ctx| store_ctx.current_cert.to_der == cert.to_der end req = Net::HTTP::Get.new("/") req["Content-Type"] = "application/x-www-form-urlencoded" http.request(req){|res| assert_equal("SSL GET / ", res.body, s_log.call + log.call) } req = Net::HTTP::Post.new("/") req["Content-Type"] = "application/x-www-form-urlencoded" req.body = "post-data" http.request(req){|res| assert_equal("SSL POST / post-data", res.body, s_log.call + log.call) } } } end if defined?(OpenSSL::SSL) def test_upstream_proxy # Testing GET or POST through the upstream proxy server # Note that the upstream proxy server works as the origin server. # +------+ # V | # client -------> proxy -------> proxy ---+ # GET / POST GET / POST GET / POST # up_proxy_handler_called = up_request_handler_called = 0 proxy_handler_called = request_handler_called = 0 up_config = { :ServerName => "localhost.localdomain", :ProxyContentHandler => Proc.new{|req, res| up_proxy_handler_called += 1}, :RequestCallback => Proc.new{|req, res| up_request_handler_called += 1} } TestWEBrick.start_httpproxy(up_config){|up_server, up_addr, up_port, up_log| up_server.mount_proc("/"){|req, res| res.body = "#{req.request_method} #{req.path} #{req.body}" } config = { :ServerName => "localhost.localdomain", :ProxyURI => URI.parse("http://localhost:#{up_port}"), :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1}, :RequestCallback => Proc.new{|req, res| request_handler_called += 1}, } TestWEBrick.start_httpproxy(config){|server, addr, port, log| http = Net::HTTP.new(up_addr, up_port, addr, port) req = Net::HTTP::Get.new("/") http.request(req){|res| skip res.message unless res.code == '200' via = res["via"].split(/,\s+/) assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call) assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call) assert_equal("GET / ", res.body) } assert_equal(1, up_proxy_handler_called, up_log.call + log.call) assert_equal(2, up_request_handler_called, up_log.call + log.call) assert_equal(1, proxy_handler_called, up_log.call + log.call) assert_equal(1, request_handler_called, up_log.call + log.call) req = Net::HTTP::Head.new("/") http.request(req){|res| via = res["via"].split(/,\s+/) assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call) assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call) assert_nil(res.body, up_log.call + log.call) } assert_equal(2, up_proxy_handler_called, up_log.call + log.call) assert_equal(4, up_request_handler_called, up_log.call + log.call) assert_equal(2, proxy_handler_called, up_log.call + log.call) assert_equal(2, request_handler_called, up_log.call + log.call) req = Net::HTTP::Post.new("/") req.body = "post-data" req.content_type = "application/x-www-form-urlencoded" http.request(req){|res| via = res["via"].split(/,\s+/) assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call) assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call) assert_equal("POST / post-data", res.body, up_log.call + log.call) } assert_equal(3, up_proxy_handler_called, up_log.call + log.call) assert_equal(6, up_request_handler_called, up_log.call + log.call) assert_equal(3, proxy_handler_called, up_log.call + log.call) assert_equal(3, request_handler_called, up_log.call + log.call) if defined?(OpenSSL::SSL) # Testing CONNECT to the upstream proxy server # # client -------> proxy -------> proxy -------> https # 1. CONNECT CONNECT establish TCP # 2. -------- establish SSL session ------> # 3. ---------- GET or POST --------------> # key = TEST_KEY_RSA2048 cert = make_certificate(key, "127.0.0.1") s_config = { :SSLEnable =>true, :ServerName => "localhost", :SSLCertificate => cert, :SSLPrivateKey => key, } TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port, s_log| s_server.mount_proc("/"){|req2, res| res.body = "SSL #{req2.request_method} #{req2.path} #{req2.body}" } http = Net::HTTP.new("127.0.0.1", s_port, addr, port, up_log.call + log.call + s_log.call) http.use_ssl = true http.verify_callback = Proc.new do |preverify_ok, store_ctx| store_ctx.current_cert.to_der == cert.to_der end req2 = Net::HTTP::Get.new("/") http.request(req2){|res| assert_equal("SSL GET / ", res.body, up_log.call + log.call + s_log.call) } req2 = Net::HTTP::Post.new("/") req2.body = "post-data" req2.content_type = "application/x-www-form-urlencoded" http.request(req2){|res| assert_equal("SSL POST / post-data", res.body, up_log.call + log.call + s_log.call) } } end } } end if defined?(OpenSSL::SSL) TEST_KEY_RSA2048 = OpenSSL::PKey.read <<-_end_of_pem_ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAuV9ht9J7k4NBs38jOXvvTKY9gW8nLICSno5EETR1cuF7i4pN s9I1QJGAFAX0BEO4KbzXmuOvfCpD3CU+Slp1enenfzq/t/e/1IRW0wkJUJUFQign 4CtrkJL+P07yx18UjyPlBXb81ApEmAB5mrJVSrWmqbjs07JbuS4QQGGXLc+Su96D kYKmSNVjBiLxVVSpyZfAY3hD37d60uG+X8xdW5v68JkRFIhdGlb6JL8fllf/A/bl NwdJOhVr9mESHhwGjwfSeTDPfd8ZLE027E5lyAVX9KZYcU00mOX+fdxOSnGqS/8J DRh0EPHDL15RcJjV2J6vZjPb0rOYGDoMcH+94wIDAQABAoIBAAzsamqfYQAqwXTb I0CJtGg6msUgU7HVkOM+9d3hM2L791oGHV6xBAdpXW2H8LgvZHJ8eOeSghR8+dgq PIqAffo4x1Oma+FOg3A0fb0evyiACyrOk+EcBdbBeLo/LcvahBtqnDfiUMQTpy6V seSoFCwuN91TSCeGIsDpRjbG1vxZgtx+uI+oH5+ytqJOmfCksRDCkMglGkzyfcl0 Xc5CUhIJ0my53xijEUQl19rtWdMnNnnkdbG8PT3LZlOta5Do86BElzUYka0C6dUc VsBDQ0Nup0P6rEQgy7tephHoRlUGTYamsajGJaAo1F3IQVIrRSuagi7+YpSpCqsW wORqorkCgYEA7RdX6MDVrbw7LePnhyuaqTiMK+055/R1TqhB1JvvxJ1CXk2rDL6G 0TLHQ7oGofd5LYiemg4ZVtWdJe43BPZlVgT6lvL/iGo8JnrncB9Da6L7nrq/+Rvj XGjf1qODCK+LmreZWEsaLPURIoR/Ewwxb9J2zd0CaMjeTwafJo1CZvcCgYEAyCgb aqoWvUecX8VvARfuA593Lsi50t4MEArnOXXcd1RnXoZWhbx5rgO8/ATKfXr0BK/n h2GF9PfKzHFm/4V6e82OL7gu/kLy2u9bXN74vOvWFL5NOrOKPM7Kg+9I131kNYOw Ivnr/VtHE5s0dY7JChYWE1F3vArrOw3T00a4CXUCgYEA0SqY+dS2LvIzW4cHCe9k IQqsT0yYm5TFsUEr4sA3xcPfe4cV8sZb9k/QEGYb1+SWWZ+AHPV3UW5fl8kTbSNb v4ng8i8rVVQ0ANbJO9e5CUrepein2MPL0AkOATR8M7t7dGGpvYV0cFk8ZrFx0oId U0PgYDotF/iueBWlbsOM430CgYEAqYI95dFyPI5/AiSkY5queeb8+mQH62sdcCCr vd/w/CZA/K5sbAo4SoTj8dLk4evU6HtIa0DOP63y071eaxvRpTNqLUOgmLh+D6gS Cc7TfLuFrD+WDBatBd5jZ+SoHccVrLR/4L8jeodo5FPW05A+9gnKXEXsTxY4LOUC 9bS4e1kCgYAqVXZh63JsMwoaxCYmQ66eJojKa47VNrOeIZDZvd2BPVf30glBOT41 gBoDG3WMPZoQj9pb7uMcrnvs4APj2FIhMU8U15LcPAj59cD6S6rWnAxO8NFK7HQG 4Jxg3JNNf8ErQoCHb1B3oVdXJkmbJkARoDpBKmTCgKtP8ADYLmVPQw== -----END RSA PRIVATE KEY----- _end_of_pem_ end end webrick-1.7.0/test/webrick/test_httprequest.rb000066400000000000000000000370521376456024300215470ustar00rootroot00000000000000# frozen_string_literal: false require "webrick" require "stringio" require "test/unit" class TestWEBrickHTTPRequest < Test::Unit::TestCase def teardown WEBrick::Utils::TimeoutHandler.terminate super end def test_simple_request msg = <<-_end_of_message_ GET / _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert(req.meta_vars) # fails if @header was not initialized and iteration is attempted on the nil reference end def test_parse_09 msg = <<-_end_of_message_ GET / foobar # HTTP/0.9 request don't have header nor entity body. _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("0.9"), req.http_version) assert_equal(WEBrick::Config::HTTP[:ServerName], req.host) assert_equal(80, req.port) assert_equal(false, req.keep_alive?) assert_equal(nil, req.body) assert(req.query.empty?) end def test_parse_10 msg = <<-_end_of_message_ GET / HTTP/1.0 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal("GET", req.request_method) assert_equal("/", req.unparsed_uri) assert_equal(WEBrick::HTTPVersion.new("1.0"), req.http_version) assert_equal(WEBrick::Config::HTTP[:ServerName], req.host) assert_equal(80, req.port) assert_equal(false, req.keep_alive?) assert_equal(nil, req.body) assert(req.query.empty?) end def test_parse_11 msg = <<-_end_of_message_ GET /path HTTP/1.1 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal("GET", req.request_method) assert_equal("/path", req.unparsed_uri) assert_equal("", req.script_name) assert_equal("/path", req.path_info) assert_equal(WEBrick::HTTPVersion.new("1.1"), req.http_version) assert_equal(WEBrick::Config::HTTP[:ServerName], req.host) assert_equal(80, req.port) assert_equal(true, req.keep_alive?) assert_equal(nil, req.body) assert(req.query.empty?) end def test_request_uri_too_large msg = <<-_end_of_message_ GET /#{"a"*2084} HTTP/1.1 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) assert_raise(WEBrick::HTTPStatus::RequestURITooLarge){ req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) } end def test_parse_headers msg = <<-_end_of_message_ GET /path HTTP/1.1 Host: test.ruby-lang.org:8080 Connection: close Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5 Accept-Encoding: compress;q=0.5 Accept-Encoding: gzip;q=1.0, identity; q=0.4, *;q=0 Accept-Language: en;q=0.5, *; q=0 Accept-Language: ja Content-Type: text/plain Content-Length: 7 X-Empty-Header: foobar _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal( URI.parse("http://test.ruby-lang.org:8080/path"), req.request_uri) assert_equal("test.ruby-lang.org", req.host) assert_equal(8080, req.port) assert_equal(false, req.keep_alive?) assert_equal( %w(text/html;level=1 text/html */* text/html;level=2 text/*), req.accept) assert_equal(%w(gzip compress identity *), req.accept_encoding) assert_equal(%w(ja en *), req.accept_language) assert_equal(7, req.content_length) assert_equal("text/plain", req.content_type) assert_equal("foobar\n", req.body) assert_equal("", req["x-empty-header"]) assert_equal(nil, req["x-no-header"]) assert(req.query.empty?) end def test_parse_header2() msg = <<-_end_of_message_ POST /foo/bar/../baz?q=a HTTP/1.0 Content-Length: 9 User-Agent: FOO BAR BAZ hogehoge _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal("POST", req.request_method) assert_equal("/foo/baz", req.path) assert_equal("", req.script_name) assert_equal("/foo/baz", req.path_info) assert_equal("9", req['content-length']) assert_equal("FOO BAR BAZ", req['user-agent']) assert_equal("hogehoge\n", req.body) end def test_parse_headers3 msg = <<-_end_of_message_ GET /path HTTP/1.1 Host: test.ruby-lang.org _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal(URI.parse("http://test.ruby-lang.org/path"), req.request_uri) assert_equal("test.ruby-lang.org", req.host) assert_equal(80, req.port) msg = <<-_end_of_message_ GET /path HTTP/1.1 Host: 192.168.1.1 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal(URI.parse("http://192.168.1.1/path"), req.request_uri) assert_equal("192.168.1.1", req.host) assert_equal(80, req.port) msg = <<-_end_of_message_ GET /path HTTP/1.1 Host: [fe80::208:dff:feef:98c7] _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]/path"), req.request_uri) assert_equal("[fe80::208:dff:feef:98c7]", req.host) assert_equal(80, req.port) msg = <<-_end_of_message_ GET /path HTTP/1.1 Host: 192.168.1.1:8080 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal(URI.parse("http://192.168.1.1:8080/path"), req.request_uri) assert_equal("192.168.1.1", req.host) assert_equal(8080, req.port) msg = <<-_end_of_message_ GET /path HTTP/1.1 Host: [fe80::208:dff:feef:98c7]:8080 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]:8080/path"), req.request_uri) assert_equal("[fe80::208:dff:feef:98c7]", req.host) assert_equal(8080, req.port) end def test_parse_get_params param = "foo=1;foo=2;foo=3;bar=x" msg = <<-_end_of_message_ GET /path?#{param} HTTP/1.1 Host: test.ruby-lang.org:8080 _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) assert_equal(["1", "2", "3"], query["foo"].list) assert_equal("x", query["bar"]) assert_equal(["x"], query["bar"].list) end def test_parse_post_params param = "foo=1;foo=2;foo=3;bar=x" msg = <<-_end_of_message_ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Content-Length: #{param.size} Content-Type: application/x-www-form-urlencoded #{param} _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) query = req.query assert_equal("1", query["foo"]) assert_equal(["1", "2", "3"], query["foo"].to_ary) assert_equal(["1", "2", "3"], query["foo"].list) assert_equal("x", query["bar"]) assert_equal(["x"], query["bar"].list) end def test_chunked crlf = "\x0d\x0a" expect = File.binread(__FILE__).freeze msg = <<-_end_of_message_ POST /path HTTP/1.1 Host: test.ruby-lang.org:8080 Transfer-Encoding: chunked _end_of_message_ msg.gsub!(/^ {6}/, "") open(__FILE__){|io| while chunk = io.read(100) msg << chunk.size.to_s(16) << crlf msg << chunk << crlf end } msg << "0" << crlf req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal(expect, req.body) # chunked req.body_reader req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) dst = StringIO.new IO.copy_stream(req.body_reader, dst) assert_equal(expect, dst.string) end def test_forwarded msg = <<-_end_of_message_ GET /foo HTTP/1.1 Host: localhost:10080 User-Agent: w3m/0.5.2 X-Forwarded-For: 123.123.123.123 X-Forwarded-Host: forward.example.com X-Forwarded-Server: server.example.com Connection: Keep-Alive _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) assert_equal("http://forward.example.com/foo", req.request_uri.to_s) assert_equal("forward.example.com", req.host) assert_equal(80, req.port) assert_equal("123.123.123.123", req.remote_ip) assert(!req.ssl?) msg = <<-_end_of_message_ GET /foo HTTP/1.1 Host: localhost:10080 User-Agent: w3m/0.5.2 X-Forwarded-For: 192.168.1.10, 172.16.1.1, 123.123.123.123 X-Forwarded-Host: forward.example.com:8080 X-Forwarded-Server: server.example.com Connection: Keep-Alive _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) assert_equal("http://forward.example.com:8080/foo", req.request_uri.to_s) assert_equal("forward.example.com", req.host) assert_equal(8080, req.port) assert_equal("123.123.123.123", req.remote_ip) assert(!req.ssl?) msg = <<-_end_of_message_ GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 X-Forwarded-Proto: https, http X-Forwarded-For: 192.168.1.10, 10.0.0.1, 123.123.123.123 X-Forwarded-Host: forward.example.com X-Forwarded-Server: server.example.com X-Requested-With: XMLHttpRequest Connection: Keep-Alive _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server.example.com", req.server_name) assert_equal("https://forward.example.com/foo", req.request_uri.to_s) assert_equal("forward.example.com", req.host) assert_equal(443, req.port) assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) msg = <<-_end_of_message_ GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 X-Forwarded-Proto: https X-Forwarded-For: 192.168.1.10 X-Forwarded-Host: forward1.example.com:1234, forward2.example.com:5678 X-Forwarded-Server: server1.example.com, server2.example.com X-Requested-With: XMLHttpRequest Connection: Keep-Alive _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) assert_equal("https://forward1.example.com:1234/foo", req.request_uri.to_s) assert_equal("forward1.example.com", req.host) assert_equal(1234, req.port) assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) msg = <<-_end_of_message_ GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 X-Forwarded-Proto: https X-Forwarded-For: 192.168.1.10 X-Forwarded-Host: [fd20:8b1e:b255:8154:250:56ff:fea8:4d84], forward2.example.com:5678 X-Forwarded-Server: server1.example.com, server2.example.com X-Requested-With: XMLHttpRequest Connection: Keep-Alive _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) assert_equal("https://[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]/foo", req.request_uri.to_s) assert_equal("[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]", req.host) assert_equal(443, req.port) assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) msg = <<-_end_of_message_ GET /foo HTTP/1.1 Host: localhost:10080 Client-IP: 234.234.234.234 X-Forwarded-Proto: https X-Forwarded-For: 192.168.1.10 X-Forwarded-Host: [fd20:8b1e:b255:8154:250:56ff:fea8:4d84]:1234, forward2.example.com:5678 X-Forwarded-Server: server1.example.com, server2.example.com X-Requested-With: XMLHttpRequest Connection: Keep-Alive _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert_equal("server1.example.com", req.server_name) assert_equal("https://[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]:1234/foo", req.request_uri.to_s) assert_equal("[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]", req.host) assert_equal(1234, req.port) assert_equal("234.234.234.234", req.remote_ip) assert(req.ssl?) end def test_continue_sent msg = <<-_end_of_message_ POST /path HTTP/1.1 Expect: 100-continue _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert req['expect'] l = msg.size req.continue assert_not_equal l, msg.size assert_match(/HTTP\/1.1 100 continue\r\n\r\n\z/, msg) assert !req['expect'] end def test_continue_not_sent msg = <<-_end_of_message_ POST /path HTTP/1.1 _end_of_message_ msg.gsub!(/^ {6}/, "") req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg)) assert !req['expect'] l = msg.size req.continue assert_equal l, msg.size end def test_empty_post msg = <<-_end_of_message_ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Content-Type: application/x-www-form-urlencoded _end_of_message_ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) req.body end def test_bad_messages param = "foo=1;foo=2;foo=3;bar=x" msg = <<-_end_of_message_ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Content-Type: application/x-www-form-urlencoded #{param} _end_of_message_ assert_raise(WEBrick::HTTPStatus::LengthRequired){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) req.body } msg = <<-_end_of_message_ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Content-Length: 100000 body is too short. _end_of_message_ assert_raise(WEBrick::HTTPStatus::BadRequest){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) req.body } msg = <<-_end_of_message_ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1 Host: test.ruby-lang.org:8080 Transfer-Encoding: foobar body is too short. _end_of_message_ assert_raise(WEBrick::HTTPStatus::NotImplemented){ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new(msg.gsub(/^ {6}/, ""))) req.body } end def test_eof_raised_when_line_is_nil assert_raise(WEBrick::HTTPStatus::EOFError) { req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP) req.parse(StringIO.new("")) } end end webrick-1.7.0/test/webrick/test_httpresponse.rb000066400000000000000000000160131376456024300217070ustar00rootroot00000000000000# frozen_string_literal: false require "webrick" require "minitest/autorun" require "stringio" require "net/http" module WEBrick class TestHTTPResponse < MiniTest::Unit::TestCase class FakeLogger attr_reader :messages def initialize @messages = [] end def warn msg @messages << msg end end attr_reader :config, :logger, :res def setup super @logger = FakeLogger.new @config = Config::HTTP @config[:Logger] = logger @res = HTTPResponse.new config @res.keep_alive = true end def test_prevent_response_splitting_headers_crlf res['X-header'] = "malicious\r\nCookie: cracked_indicator_for_test" io = StringIO.new res.send_response io io.rewind res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) assert_equal '500', res.code refute_match 'cracked_indicator_for_test', io.string end def test_prevent_response_splitting_cookie_headers_crlf user_input = "malicious\r\nCookie: cracked_indicator_for_test" res.cookies << WEBrick::Cookie.new('author', user_input) io = StringIO.new res.send_response io io.rewind res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) assert_equal '500', res.code refute_match 'cracked_indicator_for_test', io.string end def test_prevent_response_splitting_headers_cr res['X-header'] = "malicious\rCookie: cracked_indicator_for_test" io = StringIO.new res.send_response io io.rewind res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) assert_equal '500', res.code refute_match 'cracked_indicator_for_test', io.string end def test_prevent_response_splitting_cookie_headers_cr user_input = "malicious\rCookie: cracked_indicator_for_test" res.cookies << WEBrick::Cookie.new('author', user_input) io = StringIO.new res.send_response io io.rewind res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) assert_equal '500', res.code refute_match 'cracked_indicator_for_test', io.string end def test_prevent_response_splitting_headers_lf res['X-header'] = "malicious\nCookie: cracked_indicator_for_test" io = StringIO.new res.send_response io io.rewind res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) assert_equal '500', res.code refute_match 'cracked_indicator_for_test', io.string end def test_prevent_response_splitting_cookie_headers_lf user_input = "malicious\nCookie: cracked_indicator_for_test" res.cookies << WEBrick::Cookie.new('author', user_input) io = StringIO.new res.send_response io io.rewind res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) assert_equal '500', res.code refute_match 'cracked_indicator_for_test', io.string end def test_set_redirect_response_splitting url = "malicious\r\nCookie: cracked_indicator_for_test" assert_raises(URI::InvalidURIError) do res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) end end def test_set_redirect_html_injection url = 'http://example.com////?a' assert_raises(WEBrick::HTTPStatus::MultipleChoices) do res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url) end res.status = 300 io = StringIO.new res.send_response(io) io.rewind res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io)) assert_equal '300', res.code refute_match(/ "localhost", :SSLEnable => true, :SSLCertName => "/CN=localhost", } TestWEBrick.start_httpserver(config){|server, addr, port, log| server.mount_proc("/") {|req, res| res.body = "master" } # catch stderr in create_self_signed_cert stderr_buffer = StringIO.new old_stderr, $stderr = $stderr, stderr_buffer begin vhost_config1 = { :ServerName => "vhost1", :Port => port, :DoNotListen => true, :Logger => NoLog, :AccessLog => [], :SSLEnable => true, :SSLCertName => "/CN=vhost1", } vhost1 = WEBrick::HTTPServer.new(vhost_config1) vhost1.mount_proc("/") {|req, res| res.body = "vhost1" } server.virtual_host(vhost1) vhost_config2 = { :ServerName => "vhost2", :ServerAlias => ["vhost2alias"], :Port => port, :DoNotListen => true, :Logger => NoLog, :AccessLog => [], :SSLEnable => true, :SSLCertName => "/CN=vhost2", } vhost2 = WEBrick::HTTPServer.new(vhost_config2) vhost2.mount_proc("/") {|req, res| res.body = "vhost2" } server.virtual_host(vhost2) ensure # restore stderr $stderr = old_stderr end assert_match(/\A([.+*]+\n)+\z/, stderr_buffer.string) assert_equal("master", https_get(addr, port, "localhost", "/localhost")) assert_equal("master", https_get(addr, port, "unknown", "/unknown", "localhost")) assert_equal("vhost1", https_get(addr, port, "vhost1", "/vhost1")) assert_equal("vhost2", https_get(addr, port, "vhost2", "/vhost2")) assert_equal("vhost2", https_get(addr, port, "vhost2alias", "/vhost2alias", "vhost2")) } end def test_check_ssl_virtual config = { :ServerName => "localhost", :SSLEnable => true, :SSLCertName => "/CN=localhost", } TestWEBrick.start_httpserver(config){|server, addr, port, log| assert_raise ArgumentError do vhost = WEBrick::HTTPServer.new({:DoNotListen => true, :Logger => NoLog}) server.virtual_host(vhost) end } end end webrick-1.7.0/test/webrick/test_httpserver.rb000066400000000000000000000501461376456024300213640ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "net/http" require "webrick" require_relative "utils" class TestWEBrickHTTPServer < Test::Unit::TestCase empty_log = Object.new def empty_log.<<(str) assert_equal('', str) self end NoLog = WEBrick::Log.new(empty_log, WEBrick::BasicLog::WARN) def teardown WEBrick::Utils::TimeoutHandler.terminate super end def test_mount httpd = WEBrick::HTTPServer.new( :Logger => NoLog, :DoNotListen=>true ) httpd.mount("/", :Root) httpd.mount("/foo", :Foo) httpd.mount("/foo/bar", :Bar, :bar1) httpd.mount("/foo/bar/baz", :Baz, :baz1, :baz2) serv, opts, script_name, path_info = httpd.search_servlet("/") assert_equal(:Root, serv) assert_equal([], opts) assert_equal("", script_name) assert_equal("/", path_info) serv, opts, script_name, path_info = httpd.search_servlet("/sub") assert_equal(:Root, serv) assert_equal([], opts) assert_equal("", script_name) assert_equal("/sub", path_info) serv, opts, script_name, path_info = httpd.search_servlet("/sub/") assert_equal(:Root, serv) assert_equal([], opts) assert_equal("", script_name) assert_equal("/sub/", path_info) serv, opts, script_name, path_info = httpd.search_servlet("/foo") assert_equal(:Foo, serv) assert_equal([], opts) assert_equal("/foo", script_name) assert_equal("", path_info) serv, opts, script_name, path_info = httpd.search_servlet("/foo/") assert_equal(:Foo, serv) assert_equal([], opts) assert_equal("/foo", script_name) assert_equal("/", path_info) serv, opts, script_name, path_info = httpd.search_servlet("/foo/sub") assert_equal(:Foo, serv) assert_equal([], opts) assert_equal("/foo", script_name) assert_equal("/sub", path_info) serv, opts, script_name, path_info = httpd.search_servlet("/foo/bar") assert_equal(:Bar, serv) assert_equal([:bar1], opts) assert_equal("/foo/bar", script_name) assert_equal("", path_info) serv, opts, script_name, path_info = httpd.search_servlet("/foo/bar/baz") assert_equal(:Baz, serv) assert_equal([:baz1, :baz2], opts) assert_equal("/foo/bar/baz", script_name) assert_equal("", path_info) end class Req attr_reader :port, :host def initialize(addr, port, host) @addr, @port, @host = addr, port, host end def addr [0,0,0,@addr] end end def httpd(addr, port, host, ali) config ={ :Logger => NoLog, :DoNotListen => true, :BindAddress => addr, :Port => port, :ServerName => host, :ServerAlias => ali, } return WEBrick::HTTPServer.new(config) end def assert_eql?(v1, v2) assert_equal(v1.object_id, v2.object_id) end def test_lookup_server addr1 = "192.168.100.1" addr2 = "192.168.100.2" addrz = "192.168.100.254" local = "127.0.0.1" port1 = 80 port2 = 8080 port3 = 10080 portz = 32767 name1 = "www.example.com" name2 = "www2.example.com" name3 = "www3.example.com" namea = "www.example.co.jp" nameb = "www.example.jp" namec = "www2.example.co.jp" named = "www2.example.jp" namez = "foobar.example.com" alias1 = [namea, nameb] alias2 = [namec, named] host1 = httpd(nil, port1, name1, nil) hosts = [ host2 = httpd(addr1, port1, name1, nil), host3 = httpd(addr1, port1, name2, alias1), host4 = httpd(addr1, port2, name1, nil), host5 = httpd(addr1, port2, name2, alias1), httpd(addr1, port2, name3, alias2), host7 = httpd(addr2, nil, name1, nil), host8 = httpd(addr2, nil, name2, alias1), httpd(addr2, nil, name3, alias2), host10 = httpd(local, nil, nil, nil), host11 = httpd(nil, port3, nil, nil), ].sort_by{ rand } hosts.each{|h| host1.virtual_host(h) } # connect to addr1 assert_eql?(host2, host1.lookup_server(Req.new(addr1, port1, name1))) assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, name2))) assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, namea))) assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addr1, port1, namez))) assert_eql?(host4, host1.lookup_server(Req.new(addr1, port2, name1))) assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, name2))) assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, namea))) assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addr1, port2, namez))) assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, name1))) assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, name2))) assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, namea))) assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, nameb))) assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, namez))) assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, name1))) assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, name2))) assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, namea))) assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, namez))) # connect to addr2 assert_eql?(host7, host1.lookup_server(Req.new(addr2, port1, name1))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, name2))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, namea))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addr2, port1, namez))) assert_eql?(host7, host1.lookup_server(Req.new(addr2, port2, name1))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, name2))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, namea))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addr2, port2, namez))) assert_eql?(host7, host1.lookup_server(Req.new(addr2, port3, name1))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, name2))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, namea))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, nameb))) assert_eql?(host11, host1.lookup_server(Req.new(addr2, port3, namez))) assert_eql?(host7, host1.lookup_server(Req.new(addr2, portz, name1))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, name2))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, namea))) assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addr2, portz, namez))) # connect to addrz assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, name1))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, name2))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, namea))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, namez))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, name1))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, name2))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, namea))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, namez))) assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, name1))) assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, name2))) assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, namea))) assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, nameb))) assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, namez))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, name1))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, name2))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, namea))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, nameb))) assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, namez))) # connect to localhost assert_eql?(host10, host1.lookup_server(Req.new(local, port1, name1))) assert_eql?(host10, host1.lookup_server(Req.new(local, port1, name2))) assert_eql?(host10, host1.lookup_server(Req.new(local, port1, namea))) assert_eql?(host10, host1.lookup_server(Req.new(local, port1, nameb))) assert_eql?(host10, host1.lookup_server(Req.new(local, port1, namez))) assert_eql?(host10, host1.lookup_server(Req.new(local, port2, name1))) assert_eql?(host10, host1.lookup_server(Req.new(local, port2, name2))) assert_eql?(host10, host1.lookup_server(Req.new(local, port2, namea))) assert_eql?(host10, host1.lookup_server(Req.new(local, port2, nameb))) assert_eql?(host10, host1.lookup_server(Req.new(local, port2, namez))) assert_eql?(host10, host1.lookup_server(Req.new(local, port3, name1))) assert_eql?(host10, host1.lookup_server(Req.new(local, port3, name2))) assert_eql?(host10, host1.lookup_server(Req.new(local, port3, namea))) assert_eql?(host10, host1.lookup_server(Req.new(local, port3, nameb))) assert_eql?(host10, host1.lookup_server(Req.new(local, port3, namez))) assert_eql?(host10, host1.lookup_server(Req.new(local, portz, name1))) assert_eql?(host10, host1.lookup_server(Req.new(local, portz, name2))) assert_eql?(host10, host1.lookup_server(Req.new(local, portz, namea))) assert_eql?(host10, host1.lookup_server(Req.new(local, portz, nameb))) assert_eql?(host10, host1.lookup_server(Req.new(local, portz, namez))) end def test_callbacks accepted = started = stopped = 0 requested0 = requested1 = 0 config = { :ServerName => "localhost", :AcceptCallback => Proc.new{ accepted += 1 }, :StartCallback => Proc.new{ started += 1 }, :StopCallback => Proc.new{ stopped += 1 }, :RequestCallback => Proc.new{|req, res| requested0 += 1 }, } log_tester = lambda {|log, access_log| assert(log.find {|s| %r{ERROR `/' not found\.} =~ s }) assert_equal([], log.reject {|s| %r{ERROR `/' not found\.} =~ s }) } TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| vhost_config = { :ServerName => "myhostname", :BindAddress => addr, :Port => port, :DoNotListen => true, :Logger => NoLog, :AccessLog => [], :RequestCallback => Proc.new{|req, res| requested1 += 1 }, } server.virtual_host(WEBrick::HTTPServer.new(vhost_config)) Thread.pass while server.status != :Running sleep 1 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # server.status behaves unexpectedly with --jit-wait assert_equal(1, started, log.call) assert_equal(0, stopped, log.call) assert_equal(0, accepted, log.call) http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/") req["Host"] = "myhostname:#{port}" http.request(req){|res| assert_equal("404", res.code, log.call)} http.request(req){|res| assert_equal("404", res.code, log.call)} http.request(req){|res| assert_equal("404", res.code, log.call)} req["Host"] = "localhost:#{port}" http.request(req){|res| assert_equal("404", res.code, log.call)} http.request(req){|res| assert_equal("404", res.code, log.call)} http.request(req){|res| assert_equal("404", res.code, log.call)} assert_equal(6, accepted, log.call) assert_equal(3, requested0, log.call) assert_equal(3, requested1, log.call) } assert_equal(started, 1) assert_equal(stopped, 1) end class CustomRequest < ::WEBrick::HTTPRequest; end class CustomResponse < ::WEBrick::HTTPResponse; end class CustomServer < ::WEBrick::HTTPServer def create_request(config) CustomRequest.new(config) end def create_response(config) CustomResponse.new(config) end end def test_custom_server_request_and_response config = { :ServerName => "localhost" } TestWEBrick.start_server(CustomServer, config){|server, addr, port, log| server.mount_proc("/", lambda {|req, res| assert_kind_of(CustomRequest, req) assert_kind_of(CustomResponse, res) res.body = "via custom response" }) Thread.pass while server.status != :Running Net::HTTP.start(addr, port) do |http| req = Net::HTTP::Get.new("/") http.request(req){|res| assert_equal("via custom response", res.body) } server.shutdown end } end # This class is needed by test_response_io_with_chunked_set method class EventManagerForChunkedResponseTest def initialize @listeners = [] end def add_listener( &block ) @listeners << block end def raise_str_event( str ) @listeners.each{ |e| e.call( :str, str ) } end def raise_close_event() @listeners.each{ |e| e.call( :cls ) } end end def test_response_io_with_chunked_set evt_man = EventManagerForChunkedResponseTest.new t = Thread.new do begin config = { :ServerName => "localhost" } TestWEBrick.start_httpserver(config) do |server, addr, port, log| body_strs = [ 'aaaaaa', 'bb', 'cccc' ] server.mount_proc( "/", ->( req, res ){ # Test for setting chunked... res.chunked = true r,w = IO.pipe evt_man.add_listener do |type,str| type == :cls ? ( w.close ) : ( w << str ) end res.body = r } ) Thread.pass while server.status != :Running http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/") http.request(req) do |res| i = 0 evt_man.raise_str_event( body_strs[i] ) res.read_body do |s| assert_equal( body_strs[i], s ) i += 1 if i < body_strs.length evt_man.raise_str_event( body_strs[i] ) else evt_man.raise_close_event() end end assert_equal( body_strs.length, i ) end end rescue => err flunk( 'exception raised in thread: ' + err.to_s ) end end if t.join( 3 ).nil? evt_man.raise_close_event() flunk( 'timeout' ) if t.join( 1 ).nil? Thread.kill t end end end def test_response_io_without_chunked_set config = { :ServerName => "localhost" } log_tester = lambda {|log, access_log| assert_equal(1, log.length) assert_match(/WARN Could not determine content-length of response body./, log[0]) } TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| server.mount_proc("/", lambda { |req, res| r,w = IO.pipe # Test for not setting chunked... # res.chunked = true res.body = r w << "foo" w.close }) Thread.pass while server.status != :Running http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/") req['Connection'] = 'Keep-Alive' begin Timeout.timeout(2) do http.request(req){|res| assert_equal("foo", res.body) } end rescue Timeout::Error flunk('corrupted response') end } end def test_request_handler_callback_is_deprecated requested = 0 config = { :ServerName => "localhost", :RequestHandler => Proc.new{|req, res| requested += 1 }, } log_tester = lambda {|log, access_log| assert_equal(2, log.length) assert_match(/WARN :RequestHandler is deprecated, please use :RequestCallback/, log[0]) assert_match(%r{ERROR `/' not found\.}, log[1]) } TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log| Thread.pass while server.status != :Running http = Net::HTTP.new(addr, port) req = Net::HTTP::Get.new("/") req["Host"] = "localhost:#{port}" http.request(req){|res| assert_equal("404", res.code, log.call)} assert_match(%r{:RequestHandler is deprecated, please use :RequestCallback$}, log.call, log.call) } assert_equal(1, requested) end def test_shutdown_with_busy_keepalive_connection requested = 0 config = { :ServerName => "localhost", } TestWEBrick.start_httpserver(config){|server, addr, port, log| server.mount_proc("/", lambda {|req, res| res.body = "heffalump" }) Thread.pass while server.status != :Running Net::HTTP.start(addr, port) do |http| req = Net::HTTP::Get.new("/") http.request(req){|res| assert_equal('Keep-Alive', res['Connection'], log.call) } server.shutdown begin 10.times {|n| http.request(req); requested += 1 } rescue # Errno::ECONNREFUSED or similar end end } assert_equal(0, requested, "Server responded to #{requested} requests after shutdown") end def test_cntrl_in_path log_ary = [] access_log_ary = [] config = { :Port => 0, :BindAddress => '127.0.0.1', :Logger => WEBrick::Log.new(log_ary, WEBrick::BasicLog::WARN), :AccessLog => [[access_log_ary, '']], } s = WEBrick::HTTPServer.new(config) s.mount('/foo', WEBrick::HTTPServlet::FileHandler, __FILE__) th = Thread.new { s.start } addr = s.listeners[0].addr http = Net::HTTP.new(addr[3], addr[1]) req = Net::HTTP::Get.new('/notexist%0a/foo') http.request(req) { |res| assert_equal('404', res.code) } exp = %Q(ERROR `/notexist\\n/foo' not found.\n) assert_equal 1, log_ary.size assert_include log_ary[0], exp ensure s&.shutdown th&.join end def test_gigantic_request_header log_tester = lambda {|log, access_log| assert_equal 1, log.size assert_include log[0], 'ERROR headers too large' } TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log| server.mount('/', WEBrick::HTTPServlet::FileHandler, __FILE__) TCPSocket.open(addr, port) do |c| c.write("GET / HTTP/1.0\r\n") junk = -"X-Junk: #{' ' * 1024}\r\n" assert_raise(Errno::ECONNRESET, Errno::EPIPE, Errno::EPROTOTYPE) do loop { c.write(junk) } end end } end def test_eof_in_chunk log_tester = lambda do |log, access_log| assert_equal 1, log.size assert_include log[0], 'ERROR bad chunk data size' end TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log| server.mount_proc('/', ->(req, res) { res.body = req.body }) TCPSocket.open(addr, port) do |c| c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \ "Transfer-Encoding: chunked\r\n\r\n5\r\na") c.shutdown(Socket::SHUT_WR) # trigger EOF in server res = c.read assert_match %r{\AHTTP/1\.1 400 }, res end } end def test_big_chunks nr_out = 3 buf = 'big' # 3 bytes is bigger than 2! config = { :InputBufferSize => 2 }.freeze total = 0 all = '' TestWEBrick.start_httpserver(config){|server, addr, port, log| server.mount_proc('/', ->(req, res) { err = [] ret = req.body do |chunk| n = chunk.bytesize n > config[:InputBufferSize] and err << "#{n} > :InputBufferSize" total += n all << chunk end ret.nil? or err << 'req.body should return nil' (buf * nr_out) == all or err << 'input body does not match expected' res.header['connection'] = 'close' res.body = err.join("\n") }) TCPSocket.open(addr, port) do |c| c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \ "Transfer-Encoding: chunked\r\n\r\n") chunk = "#{buf.bytesize.to_s(16)}\r\n#{buf}\r\n" nr_out.times { c.write(chunk) } c.write("0\r\n\r\n") head, body = c.read.split("\r\n\r\n") assert_match %r{\AHTTP/1\.1 200 OK}, head assert_nil body end } end end webrick-1.7.0/test/webrick/test_httpstatus.rb000066400000000000000000000015141376456024300213740ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick" class TestWEBrickHTTPStatus < Test::Unit::TestCase def test_info? assert WEBrick::HTTPStatus.info?(100) refute WEBrick::HTTPStatus.info?(200) end def test_success? assert WEBrick::HTTPStatus.success?(200) refute WEBrick::HTTPStatus.success?(300) end def test_redirect? assert WEBrick::HTTPStatus.redirect?(300) refute WEBrick::HTTPStatus.redirect?(400) end def test_error? assert WEBrick::HTTPStatus.error?(400) refute WEBrick::HTTPStatus.error?(600) end def test_client_error? assert WEBrick::HTTPStatus.client_error?(400) refute WEBrick::HTTPStatus.client_error?(500) end def test_server_error? assert WEBrick::HTTPStatus.server_error?(500) refute WEBrick::HTTPStatus.server_error?(600) end end webrick-1.7.0/test/webrick/test_httputils.rb000066400000000000000000000110041376456024300212040ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick/httputils" class TestWEBrickHTTPUtils < Test::Unit::TestCase include WEBrick::HTTPUtils def test_normilize_path assert_equal("/foo", normalize_path("/foo")) assert_equal("/foo/bar/", normalize_path("/foo/bar/")) assert_equal("/", normalize_path("/foo/../")) assert_equal("/", normalize_path("/foo/..")) assert_equal("/", normalize_path("/foo/bar/../../")) assert_equal("/", normalize_path("/foo/bar/../..")) assert_equal("/", normalize_path("/foo/bar/../..")) assert_equal("/baz", normalize_path("/foo/bar/../../baz")) assert_equal("/baz", normalize_path("/foo/../bar/../baz")) assert_equal("/baz/", normalize_path("/foo/../bar/../baz/")) assert_equal("/...", normalize_path("/bar/../...")) assert_equal("/.../", normalize_path("/bar/../.../")) assert_equal("/foo/", normalize_path("/foo/./")) assert_equal("/foo/", normalize_path("/foo/.")) assert_equal("/foo/", normalize_path("/foo/././")) assert_equal("/foo/", normalize_path("/foo/./.")) assert_equal("/foo/bar", normalize_path("/foo/./bar")) assert_equal("/foo/bar/", normalize_path("/foo/./bar/.")) assert_equal("/foo/bar/", normalize_path("/./././foo/./bar/.")) assert_equal("/foo/bar/", normalize_path("//foo///.//bar/.///.//")) assert_equal("/", normalize_path("//foo///..///bar/.///..//.//")) assert_raise(RuntimeError){ normalize_path("foo/bar") } assert_raise(RuntimeError){ normalize_path("..") } assert_raise(RuntimeError){ normalize_path("/..") } assert_raise(RuntimeError){ normalize_path("/./..") } assert_raise(RuntimeError){ normalize_path("/./../") } assert_raise(RuntimeError){ normalize_path("/./../..") } assert_raise(RuntimeError){ normalize_path("/./../../") } assert_raise(RuntimeError){ normalize_path("/./../") } assert_raise(RuntimeError){ normalize_path("/../..") } assert_raise(RuntimeError){ normalize_path("/../../") } assert_raise(RuntimeError){ normalize_path("/../../..") } assert_raise(RuntimeError){ normalize_path("/../../../") } assert_raise(RuntimeError){ normalize_path("/../foo/../") } assert_raise(RuntimeError){ normalize_path("/../foo/../../") } assert_raise(RuntimeError){ normalize_path("/foo/bar/../../../../") } assert_raise(RuntimeError){ normalize_path("/foo/../bar/../../") } assert_raise(RuntimeError){ normalize_path("/./../bar/") } assert_raise(RuntimeError){ normalize_path("/./../") } end def test_split_header_value assert_equal(['foo', 'bar'], split_header_value('foo, bar')) assert_equal(['"foo"', 'bar'], split_header_value('"foo", bar')) assert_equal(['foo', '"bar"'], split_header_value('foo, "bar"')) assert_equal(['*'], split_header_value('*')) assert_equal(['W/"xyzzy"', 'W/"r2d2xxxx"', 'W/"c3piozzzz"'], split_header_value('W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"')) end def test_escape assert_equal("/foo/bar", escape("/foo/bar")) assert_equal("/~foo/bar", escape("/~foo/bar")) assert_equal("/~foo%20bar", escape("/~foo bar")) assert_equal("/~foo%20bar", escape("/~foo bar")) assert_equal("/~foo%09bar", escape("/~foo\tbar")) assert_equal("/~foo+bar", escape("/~foo+bar")) bug8425 = '[Bug #8425] [ruby-core:55052]' assert_nothing_raised(ArgumentError, Encoding::CompatibilityError, bug8425) { assert_equal("%E3%83%AB%E3%83%93%E3%83%BC%E3%81%95%E3%82%93", escape("\u{30EB 30D3 30FC 3055 3093}")) } end def test_escape_form assert_equal("%2Ffoo%2Fbar", escape_form("/foo/bar")) assert_equal("%2F~foo%2Fbar", escape_form("/~foo/bar")) assert_equal("%2F~foo+bar", escape_form("/~foo bar")) assert_equal("%2F~foo+%2B+bar", escape_form("/~foo + bar")) end def test_unescape assert_equal("/foo/bar", unescape("%2ffoo%2fbar")) assert_equal("/~foo/bar", unescape("/%7efoo/bar")) assert_equal("/~foo/bar", unescape("%2f%7efoo%2fbar")) assert_equal("/~foo+bar", unescape("/%7efoo+bar")) end def test_unescape_form assert_equal("//foo/bar", unescape_form("/%2Ffoo/bar")) assert_equal("//foo/bar baz", unescape_form("/%2Ffoo/bar+baz")) assert_equal("/~foo/bar baz", unescape_form("/%7Efoo/bar+baz")) end def test_escape_path assert_equal("/foo/bar", escape_path("/foo/bar")) assert_equal("/foo/bar/", escape_path("/foo/bar/")) assert_equal("/%25foo/bar/", escape_path("/%foo/bar/")) end end webrick-1.7.0/test/webrick/test_httpversion.rb000066400000000000000000000016601376456024300215400ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick/httpversion" class TestWEBrickHTTPVersion < Test::Unit::TestCase def setup @v09 = WEBrick::HTTPVersion.new("0.9") @v10 = WEBrick::HTTPVersion.new("1.0") @v11 = WEBrick::HTTPVersion.new("1.001") end def test_to_s() assert_equal("0.9", @v09.to_s) assert_equal("1.0", @v10.to_s) assert_equal("1.1", @v11.to_s) end def test_major() assert_equal(0, @v09.major) assert_equal(1, @v10.major) assert_equal(1, @v11.major) end def test_minor() assert_equal(9, @v09.minor) assert_equal(0, @v10.minor) assert_equal(1, @v11.minor) end def test_compar() assert_equal(0, @v09 <=> "0.9") assert_equal(0, @v09 <=> "0.09") assert_equal(-1, @v09 <=> @v10) assert_equal(-1, @v09 <=> "1.00") assert_equal(1, @v11 <=> @v09) assert_equal(1, @v11 <=> "1.0") assert_equal(1, @v11 <=> "0.9") end end webrick-1.7.0/test/webrick/test_server.rb000066400000000000000000000124341376456024300204620ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "tempfile" require "webrick" require_relative "utils" class TestWEBrickServer < Test::Unit::TestCase class Echo < WEBrick::GenericServer def run(sock) while line = sock.gets sock << line end end end def test_server TestWEBrick.start_server(Echo){|server, addr, port, log| TCPSocket.open(addr, port){|sock| sock.puts("foo"); assert_equal("foo\n", sock.gets, log.call) sock.puts("bar"); assert_equal("bar\n", sock.gets, log.call) sock.puts("baz"); assert_equal("baz\n", sock.gets, log.call) sock.puts("qux"); assert_equal("qux\n", sock.gets, log.call) } } end def test_start_exception stopped = 0 log = [] logger = WEBrick::Log.new(log, WEBrick::BasicLog::WARN) assert_raise(SignalException) do listener = Object.new def listener.to_io # IO.select invokes #to_io. raise SignalException, 'SIGTERM' # simulate signal in main thread end def listener.shutdown end def listener.close end server = WEBrick::HTTPServer.new({ :BindAddress => "127.0.0.1", :Port => 0, :StopCallback => Proc.new{ stopped += 1 }, :Logger => logger, }) server.listeners[0].close server.listeners[0] = listener server.start end assert_equal(1, stopped) assert_equal(1, log.length) assert_match(/FATAL SignalException: SIGTERM/, log[0]) end def test_callbacks accepted = started = stopped = 0 config = { :AcceptCallback => Proc.new{ accepted += 1 }, :StartCallback => Proc.new{ started += 1 }, :StopCallback => Proc.new{ stopped += 1 }, } TestWEBrick.start_server(Echo, config){|server, addr, port, log| true while server.status != :Running sleep 1 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # server.status behaves unexpectedly with --jit-wait assert_equal(1, started, log.call) assert_equal(0, stopped, log.call) assert_equal(0, accepted, log.call) TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets } assert_equal(3, accepted, log.call) } assert_equal(1, started) assert_equal(1, stopped) end def test_daemon begin r, w = IO.pipe pid1 = Process.fork{ r.close WEBrick::Daemon.start w.puts(Process.pid) sleep 10 } pid2 = r.gets.to_i assert(Process.kill(:KILL, pid2)) assert_not_equal(pid1, pid2) rescue NotImplementedError # snip this test ensure Process.wait(pid1) if pid1 r.close w.close end end def test_restart_after_shutdown address = '127.0.0.1' port = 0 log = [] config = { :BindAddress => address, :Port => port, :Logger => WEBrick::Log.new(log, WEBrick::BasicLog::WARN), } server = Echo.new(config) client_proc = lambda {|str| begin ret = server.listeners.first.connect_address.connect {|s| s.write(str) s.close_write s.read } assert_equal(str, ret) ensure server.shutdown end } server_thread = Thread.new { server.start } client_thread = Thread.new { client_proc.call("a") } assert_join_threads([client_thread, server_thread]) server.listen(address, port) server_thread = Thread.new { server.start } client_thread = Thread.new { client_proc.call("b") } assert_join_threads([client_thread, server_thread]) assert_equal([], log) end def test_restart_after_stop log = Object.new class << log include Test::Unit::Assertions def <<(msg) flunk "unexpected log: #{msg.inspect}" end end client_thread = nil wakeup = -> {client_thread.wakeup} warn_flunk = WEBrick::Log.new(log, WEBrick::BasicLog::WARN) server = WEBrick::HTTPServer.new( :StartCallback => wakeup, :StopCallback => wakeup, :BindAddress => '0.0.0.0', :Port => 0, :Logger => warn_flunk) 2.times { server_thread = Thread.start { server.start } client_thread = Thread.start { sleep 0.1 until server.status == :Running || !server_thread.status server.stop sleep 0.1 until server.status == :Stop || !server_thread.status } assert_join_threads([client_thread, server_thread]) } end def test_port_numbers config = { :BindAddress => '0.0.0.0', :Logger => WEBrick::Log.new([], WEBrick::BasicLog::WARN), } ports = [0, "0"] ports.each do |port| config[:Port]= port server = WEBrick::GenericServer.new(config) server_thread = Thread.start { server.start } client_thread = Thread.start { sleep 0.1 until server.status == :Running || !server_thread.status server_port = server.listeners[0].addr[1] server.stop assert_equal server.config[:Port], server_port sleep 0.1 until server.status == :Stop || !server_thread.status } assert_join_threads([client_thread, server_thread]) end assert_raise(ArgumentError) do config[:Port]= "FOO" WEBrick::GenericServer.new(config) end end end webrick-1.7.0/test/webrick/test_ssl_server.rb000066400000000000000000000032701376456024300213410ustar00rootroot00000000000000require "test/unit" require "webrick" require "webrick/ssl" require_relative "utils" require 'timeout' class TestWEBrickSSLServer < Test::Unit::TestCase class Echo < WEBrick::GenericServer def run(sock) while line = sock.gets sock << line end end end def test_self_signed_cert_server assert_self_signed_cert( :SSLEnable => true, :SSLCertName => [["C", "JP"], ["O", "www.ruby-lang.org"], ["CN", "Ruby"]], ) end def test_self_signed_cert_server_with_string assert_self_signed_cert( :SSLEnable => true, :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", ) end def assert_self_signed_cert(config) TestWEBrick.start_server(Echo, config){|server, addr, port, log| io = TCPSocket.new(addr, port) sock = OpenSSL::SSL::SSLSocket.new(io) sock.connect sock.puts(server.ssl_context.cert.subject.to_s) assert_equal("/C=JP/O=www.ruby-lang.org/CN=Ruby\n", sock.gets, log.call) sock.close io.close } end def test_slow_connect poke = lambda do |io, msg| begin sock = OpenSSL::SSL::SSLSocket.new(io) sock.connect sock.puts(msg) assert_equal "#{msg}\n", sock.gets, msg ensure sock&.close io.close end end config = { :SSLEnable => true, :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby", } EnvUtil.timeout(10) do TestWEBrick.start_server(Echo, config) do |server, addr, port, log| outer = TCPSocket.new(addr, port) inner = TCPSocket.new(addr, port) poke.call(inner, 'fast TLS negotiation') poke.call(outer, 'slow TLS negotiation') end end end end webrick-1.7.0/test/webrick/test_utils.rb000066400000000000000000000051451376456024300203150ustar00rootroot00000000000000# frozen_string_literal: false require "test/unit" require "webrick/utils" class TestWEBrickUtils < Test::Unit::TestCase def teardown WEBrick::Utils::TimeoutHandler.terminate super end def assert_expired(m) Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do assert_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info)) end end def assert_not_expired(m) Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do assert_not_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info)) end end EX = Class.new(StandardError) def test_no_timeout m = WEBrick::Utils assert_equal(:foo, m.timeout(10){ :foo }) assert_expired(m) end def test_nested_timeout_outer m = WEBrick::Utils i = 0 assert_raise(Timeout::Error){ m.timeout(1){ assert_raise(Timeout::Error){ m.timeout(0.1){ i += 1; sleep(1) } } assert_not_expired(m) i += 1 sleep(2) } } assert_equal(2, i) assert_expired(m) end def test_timeout_default_exception m = WEBrick::Utils assert_raise(Timeout::Error){ m.timeout(0.01){ sleep } } assert_expired(m) end def test_timeout_custom_exception m = WEBrick::Utils ex = EX assert_raise(ex){ m.timeout(0.01, ex){ sleep } } assert_expired(m) end def test_nested_timeout_inner_custom_exception m = WEBrick::Utils ex = EX i = 0 assert_raise(ex){ m.timeout(10){ m.timeout(0.01, ex){ i += 1; sleep } } sleep } assert_equal(1, i) assert_expired(m) end def test_nested_timeout_outer_custom_exception m = WEBrick::Utils ex = EX i = 0 assert_raise(Timeout::Error){ m.timeout(0.01){ m.timeout(1.0, ex){ i += 1; sleep } } sleep } assert_equal(1, i) assert_expired(m) end def test_create_listeners addr = listener_address(0) port = addr.slice!(1) assert_kind_of(Integer, port, "dynamically chosen port number") assert_equal(["AF_INET", "127.0.0.1", "127.0.0.1"], addr) assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"], listener_address(port), "specific port number") assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"], listener_address(port.to_s), "specific port number string") end def listener_address(port) listeners = WEBrick::Utils.create_listeners("127.0.0.1", port) srv = listeners.first assert_kind_of TCPServer, srv srv.addr ensure listeners.each(&:close) if listeners end end webrick-1.7.0/test/webrick/utils.rb000066400000000000000000000052031376456024300172510ustar00rootroot00000000000000# frozen_string_literal: false require "webrick" begin require "webrick/https" rescue LoadError end require "webrick/httpproxy" module TestWEBrick NullWriter = Object.new def NullWriter.<<(msg) puts msg if $DEBUG return self end class WEBrick::HTTPServlet::CGIHandler remove_const :Ruby require "envutil" unless defined?(EnvUtil) Ruby = EnvUtil.rubybin remove_const :CGIRunner CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc: remove_const :CGIRunnerArray CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb"] # :nodoc: end RubyBin = "\"#{EnvUtil.rubybin}\"" RubyBin << " --disable-gems" RubyBin << " \"-I#{File.expand_path("../..", File.dirname(__FILE__))}/lib\"" RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/common\"" RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}\"" RubyBinArray = [EnvUtil.rubybin] RubyBinArray << "--disable-gems" RubyBinArray << "-I" << "#{File.expand_path("../..", File.dirname(__FILE__))}/lib" RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/common" RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}" require "test/unit" unless defined?(Test::Unit) include Test::Unit::Assertions extend Test::Unit::Assertions module_function DefaultLogTester = lambda {|log, access_log| assert_equal([], log) } def start_server(klass, config={}, log_tester=DefaultLogTester, &block) log_ary = [] access_log_ary = [] log = proc { "webrick log start:\n" + (log_ary+access_log_ary).join.gsub(/^/, " ").chomp + "\nwebrick log end" } config = ({ :BindAddress => "127.0.0.1", :Port => 0, :ServerType => Thread, :Logger => WEBrick::Log.new(log_ary, WEBrick::BasicLog::WARN), :AccessLog => [[access_log_ary, ""]] }.update(config)) server = capture_output {break klass.new(config)} server_thread = server.start server_thread2 = Thread.new { server_thread.join if log_tester log_tester.call(log_ary, access_log_ary) end } addr = server.listeners[0].addr client_thread = Thread.new { begin block.yield([server, addr[3], addr[1], log]) ensure server.shutdown end } assert_join_threads([client_thread, server_thread2]) end def start_httpserver(config={}, log_tester=DefaultLogTester, &block) start_server(WEBrick::HTTPServer, config, log_tester, &block) end def start_httpproxy(config={}, log_tester=DefaultLogTester, &block) start_server(WEBrick::HTTPProxyServer, config, log_tester, &block) end end webrick-1.7.0/test/webrick/webrick.cgi000066400000000000000000000017641376456024300177060ustar00rootroot00000000000000#!ruby require "webrick/cgi" class TestApp < WEBrick::CGI def do_GET(req, res) res["content-type"] = "text/plain" if req.path_info == "/dumpenv" res.body = Marshal.dump(ENV.to_hash) elsif (p = req.path_info) && p.length > 0 res.body = p elsif (q = req.query).size > 0 res.body = q.keys.sort.collect{|key| q[key].list.sort.collect{|v| "#{key}=#{v}" }.join(", ") }.join(", ") elsif %r{/$} =~ req.request_uri.to_s res.body = "" res.body << req.request_uri.to_s << "\n" res.body << req.script_name elsif !req.cookies.empty? res.body = req.cookies.inject(""){|result, cookie| result << "%s=%s\n" % [cookie.name, cookie.value] } res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE") res.cookies << WEBrick::Cookie.new("Shipping", "FedEx") else res.body = req.script_name end end def do_POST(req, res) do_GET(req, res) end end cgi = TestApp.new cgi.start webrick-1.7.0/test/webrick/webrick.rhtml000066400000000000000000000001171376456024300202610ustar00rootroot00000000000000req to <%= servlet_request.request_uri %> <%= servlet_request.query.inspect %> webrick-1.7.0/test/webrick/webrick_long_filename.cgi000066400000000000000000000016431376456024300225610ustar00rootroot00000000000000#!ruby require "webrick/cgi" class TestApp < WEBrick::CGI def do_GET(req, res) res["content-type"] = "text/plain" if (p = req.path_info) && p.length > 0 res.body = p elsif (q = req.query).size > 0 res.body = q.keys.sort.collect{|key| q[key].list.sort.collect{|v| "#{key}=#{v}" }.join(", ") }.join(", ") elsif %r{/$} =~ req.request_uri.to_s res.body = "" res.body << req.request_uri.to_s << "\n" res.body << req.script_name elsif !req.cookies.empty? res.body = req.cookies.inject(""){|result, cookie| result << "%s=%s\n" % [cookie.name, cookie.value] } res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE") res.cookies << WEBrick::Cookie.new("Shipping", "FedEx") else res.body = req.script_name end end def do_POST(req, res) do_GET(req, res) end end cgi = TestApp.new cgi.start webrick-1.7.0/webrick.gemspec000066400000000000000000000042611376456024300161550ustar00rootroot00000000000000# frozen_string_literal: true begin require_relative 'lib/webrick/version' rescue LoadError # for Ruby core repository require_relative 'version' end Gem::Specification.new do |s| s.name = "webrick" s.version = WEBrick::VERSION s.summary = "HTTP server toolkit" s.description = "WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server." s.require_path = %w{lib} s.files = [ "Gemfile", "LICENSE.txt", "README.md", "Rakefile", "bin/console", "bin/setup", "lib/webrick.rb", "lib/webrick/accesslog.rb", "lib/webrick/cgi.rb", "lib/webrick/compat.rb", "lib/webrick/config.rb", "lib/webrick/cookie.rb", "lib/webrick/htmlutils.rb", "lib/webrick/httpauth.rb", "lib/webrick/httpauth/authenticator.rb", "lib/webrick/httpauth/basicauth.rb", "lib/webrick/httpauth/digestauth.rb", "lib/webrick/httpauth/htdigest.rb", "lib/webrick/httpauth/htgroup.rb", "lib/webrick/httpauth/htpasswd.rb", "lib/webrick/httpauth/userdb.rb", "lib/webrick/httpproxy.rb", "lib/webrick/httprequest.rb", "lib/webrick/httpresponse.rb", "lib/webrick/https.rb", "lib/webrick/httpserver.rb", "lib/webrick/httpservlet.rb", "lib/webrick/httpservlet/abstract.rb", "lib/webrick/httpservlet/cgi_runner.rb", "lib/webrick/httpservlet/cgihandler.rb", "lib/webrick/httpservlet/erbhandler.rb", "lib/webrick/httpservlet/filehandler.rb", "lib/webrick/httpservlet/prochandler.rb", "lib/webrick/httpstatus.rb", "lib/webrick/httputils.rb", "lib/webrick/httpversion.rb", "lib/webrick/log.rb", "lib/webrick/server.rb", "lib/webrick/ssl.rb", "lib/webrick/utils.rb", "lib/webrick/version.rb", "webrick.gemspec", ] s.required_ruby_version = ">= 2.3.0" s.authors = ["TAKAHASHI Masayoshi", "GOTOU YUUZOU", "Eric Wong"] s.email = [nil, nil, 'normal@ruby-lang.org'] s.homepage = "https://github.com/ruby/webrick" s.licenses = ["Ruby", "BSD-2-Clause"] if s.respond_to?(:metadata=) s.metadata = { "bug_tracker_uri" => "https://github.com/ruby/webrick/issues", } end s.add_development_dependency "rake" end