From 5dc786bf86bb6e0da2639f88659598ec8b9db30d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 2 Nov 2020 13:44:28 +0900 Subject: Move webrick library into internal test toolchain --- lib/webrick.rb | 232 --------- lib/webrick/.document | 6 - lib/webrick/accesslog.rb | 157 ------ lib/webrick/cgi.rb | 313 ------------ lib/webrick/compat.rb | 36 -- lib/webrick/config.rb | 158 ------ lib/webrick/cookie.rb | 172 ------- lib/webrick/htmlutils.rb | 30 -- lib/webrick/httpauth.rb | 96 ---- lib/webrick/httpauth/authenticator.rb | 117 ----- lib/webrick/httpauth/basicauth.rb | 116 ----- lib/webrick/httpauth/digestauth.rb | 395 --------------- lib/webrick/httpauth/htdigest.rb | 132 ----- lib/webrick/httpauth/htgroup.rb | 97 ---- lib/webrick/httpauth/htpasswd.rb | 158 ------ lib/webrick/httpauth/userdb.rb | 53 -- lib/webrick/httpproxy.rb | 354 ------------- lib/webrick/httprequest.rb | 636 ------------------------ lib/webrick/httpresponse.rb | 564 --------------------- lib/webrick/https.rb | 152 ------ lib/webrick/httpserver.rb | 294 ----------- lib/webrick/httpservlet.rb | 23 - lib/webrick/httpservlet/abstract.rb | 152 ------ lib/webrick/httpservlet/cgi_runner.rb | 47 -- lib/webrick/httpservlet/cgihandler.rb | 126 ----- lib/webrick/httpservlet/erbhandler.rb | 88 ---- lib/webrick/httpservlet/filehandler.rb | 552 -------------------- lib/webrick/httpservlet/prochandler.rb | 47 -- lib/webrick/httpstatus.rb | 194 -------- lib/webrick/httputils.rb | 512 ------------------- lib/webrick/httpversion.rb | 76 --- lib/webrick/log.rb | 156 ------ lib/webrick/server.rb | 381 -------------- lib/webrick/ssl.rb | 215 -------- lib/webrick/utils.rb | 265 ---------- lib/webrick/version.rb | 18 - lib/webrick/webrick.gemspec | 76 --- test/webrick/.htaccess | 1 - test/webrick/test_cgi.rb | 170 ------- test/webrick/test_config.rb | 17 - test/webrick/test_cookie.rb | 141 ------ test/webrick/test_do_not_reverse_lookup.rb | 71 --- test/webrick/test_filehandler.rb | 402 --------------- test/webrick/test_htgroup.rb | 19 - test/webrick/test_htmlutils.rb | 21 - test/webrick/test_httpauth.rb | 366 -------------- test/webrick/test_httpproxy.rb | 466 ----------------- test/webrick/test_httprequest.rb | 488 ------------------ test/webrick/test_httpresponse.rb | 282 ----------- test/webrick/test_https.rb | 112 ----- test/webrick/test_httpserver.rb | 543 -------------------- test/webrick/test_httpstatus.rb | 35 -- test/webrick/test_httputils.rb | 101 ---- test/webrick/test_httpversion.rb | 41 -- test/webrick/test_server.rb | 191 ------- test/webrick/test_ssl_server.rb | 67 --- test/webrick/test_utils.rb | 110 ---- test/webrick/utils.rb | 82 --- test/webrick/webrick.cgi | 38 -- test/webrick/webrick.rhtml | 4 - test/webrick/webrick_long_filename.cgi | 36 -- tool/lib/webrick.rb | 232 +++++++++ tool/lib/webrick/.document | 6 + tool/lib/webrick/accesslog.rb | 157 ++++++ tool/lib/webrick/cgi.rb | 313 ++++++++++++ tool/lib/webrick/compat.rb | 36 ++ tool/lib/webrick/config.rb | 158 ++++++ tool/lib/webrick/cookie.rb | 172 +++++++ tool/lib/webrick/htmlutils.rb | 30 ++ tool/lib/webrick/httpauth.rb | 96 ++++ tool/lib/webrick/httpauth/authenticator.rb | 117 +++++ tool/lib/webrick/httpauth/basicauth.rb | 116 +++++ tool/lib/webrick/httpauth/digestauth.rb | 395 +++++++++++++++ tool/lib/webrick/httpauth/htdigest.rb | 132 +++++ tool/lib/webrick/httpauth/htgroup.rb | 97 ++++ tool/lib/webrick/httpauth/htpasswd.rb | 158 ++++++ tool/lib/webrick/httpauth/userdb.rb | 53 ++ tool/lib/webrick/httpproxy.rb | 354 +++++++++++++ tool/lib/webrick/httprequest.rb | 636 ++++++++++++++++++++++++ tool/lib/webrick/httpresponse.rb | 564 +++++++++++++++++++++ tool/lib/webrick/https.rb | 152 ++++++ tool/lib/webrick/httpserver.rb | 294 +++++++++++ tool/lib/webrick/httpservlet.rb | 23 + tool/lib/webrick/httpservlet/abstract.rb | 152 ++++++ tool/lib/webrick/httpservlet/cgi_runner.rb | 47 ++ tool/lib/webrick/httpservlet/cgihandler.rb | 126 +++++ tool/lib/webrick/httpservlet/erbhandler.rb | 88 ++++ tool/lib/webrick/httpservlet/filehandler.rb | 552 ++++++++++++++++++++ tool/lib/webrick/httpservlet/prochandler.rb | 47 ++ tool/lib/webrick/httpstatus.rb | 194 ++++++++ tool/lib/webrick/httputils.rb | 512 +++++++++++++++++++ tool/lib/webrick/httpversion.rb | 76 +++ tool/lib/webrick/log.rb | 156 ++++++ tool/lib/webrick/server.rb | 381 ++++++++++++++ tool/lib/webrick/ssl.rb | 215 ++++++++ tool/lib/webrick/utils.rb | 265 ++++++++++ tool/lib/webrick/version.rb | 18 + tool/lib/webrick/webrick.gemspec | 76 +++ tool/test/webrick/.htaccess | 1 + tool/test/webrick/test_cgi.rb | 170 +++++++ tool/test/webrick/test_config.rb | 17 + tool/test/webrick/test_cookie.rb | 141 ++++++ tool/test/webrick/test_do_not_reverse_lookup.rb | 71 +++ tool/test/webrick/test_filehandler.rb | 402 +++++++++++++++ tool/test/webrick/test_htgroup.rb | 19 + tool/test/webrick/test_htmlutils.rb | 21 + tool/test/webrick/test_httpauth.rb | 366 ++++++++++++++ tool/test/webrick/test_httpproxy.rb | 466 +++++++++++++++++ tool/test/webrick/test_httprequest.rb | 488 ++++++++++++++++++ tool/test/webrick/test_httpresponse.rb | 282 +++++++++++ tool/test/webrick/test_https.rb | 112 +++++ tool/test/webrick/test_httpserver.rb | 543 ++++++++++++++++++++ tool/test/webrick/test_httpstatus.rb | 35 ++ tool/test/webrick/test_httputils.rb | 101 ++++ tool/test/webrick/test_httpversion.rb | 41 ++ tool/test/webrick/test_server.rb | 191 +++++++ tool/test/webrick/test_ssl_server.rb | 67 +++ tool/test/webrick/test_utils.rb | 110 ++++ tool/test/webrick/utils.rb | 82 +++ tool/test/webrick/webrick.cgi | 38 ++ tool/test/webrick/webrick.rhtml | 4 + tool/test/webrick/webrick_long_filename.cgi | 36 ++ 122 files changed, 11000 insertions(+), 11000 deletions(-) delete mode 100644 lib/webrick.rb delete mode 100644 lib/webrick/.document delete mode 100644 lib/webrick/accesslog.rb delete mode 100644 lib/webrick/cgi.rb delete mode 100644 lib/webrick/compat.rb delete mode 100644 lib/webrick/config.rb delete mode 100644 lib/webrick/cookie.rb delete mode 100644 lib/webrick/htmlutils.rb delete mode 100644 lib/webrick/httpauth.rb delete mode 100644 lib/webrick/httpauth/authenticator.rb delete mode 100644 lib/webrick/httpauth/basicauth.rb delete mode 100644 lib/webrick/httpauth/digestauth.rb delete mode 100644 lib/webrick/httpauth/htdigest.rb delete mode 100644 lib/webrick/httpauth/htgroup.rb delete mode 100644 lib/webrick/httpauth/htpasswd.rb delete mode 100644 lib/webrick/httpauth/userdb.rb delete mode 100644 lib/webrick/httpproxy.rb delete mode 100644 lib/webrick/httprequest.rb delete mode 100644 lib/webrick/httpresponse.rb delete mode 100644 lib/webrick/https.rb delete mode 100644 lib/webrick/httpserver.rb delete mode 100644 lib/webrick/httpservlet.rb delete mode 100644 lib/webrick/httpservlet/abstract.rb delete mode 100644 lib/webrick/httpservlet/cgi_runner.rb delete mode 100644 lib/webrick/httpservlet/cgihandler.rb delete mode 100644 lib/webrick/httpservlet/erbhandler.rb delete mode 100644 lib/webrick/httpservlet/filehandler.rb delete mode 100644 lib/webrick/httpservlet/prochandler.rb delete mode 100644 lib/webrick/httpstatus.rb delete mode 100644 lib/webrick/httputils.rb delete mode 100644 lib/webrick/httpversion.rb delete mode 100644 lib/webrick/log.rb delete mode 100644 lib/webrick/server.rb delete mode 100644 lib/webrick/ssl.rb delete mode 100644 lib/webrick/utils.rb delete mode 100644 lib/webrick/version.rb delete mode 100644 lib/webrick/webrick.gemspec delete mode 100644 test/webrick/.htaccess delete mode 100644 test/webrick/test_cgi.rb delete mode 100644 test/webrick/test_config.rb delete mode 100644 test/webrick/test_cookie.rb delete mode 100644 test/webrick/test_do_not_reverse_lookup.rb delete mode 100644 test/webrick/test_filehandler.rb delete mode 100644 test/webrick/test_htgroup.rb delete mode 100644 test/webrick/test_htmlutils.rb delete mode 100644 test/webrick/test_httpauth.rb delete mode 100644 test/webrick/test_httpproxy.rb delete mode 100644 test/webrick/test_httprequest.rb delete mode 100644 test/webrick/test_httpresponse.rb delete mode 100644 test/webrick/test_https.rb delete mode 100644 test/webrick/test_httpserver.rb delete mode 100644 test/webrick/test_httpstatus.rb delete mode 100644 test/webrick/test_httputils.rb delete mode 100644 test/webrick/test_httpversion.rb delete mode 100644 test/webrick/test_server.rb delete mode 100644 test/webrick/test_ssl_server.rb delete mode 100644 test/webrick/test_utils.rb delete mode 100644 test/webrick/utils.rb delete mode 100644 test/webrick/webrick.cgi delete mode 100644 test/webrick/webrick.rhtml delete mode 100644 test/webrick/webrick_long_filename.cgi create mode 100644 tool/lib/webrick.rb create mode 100644 tool/lib/webrick/.document create mode 100644 tool/lib/webrick/accesslog.rb create mode 100644 tool/lib/webrick/cgi.rb create mode 100644 tool/lib/webrick/compat.rb create mode 100644 tool/lib/webrick/config.rb create mode 100644 tool/lib/webrick/cookie.rb create mode 100644 tool/lib/webrick/htmlutils.rb create mode 100644 tool/lib/webrick/httpauth.rb create mode 100644 tool/lib/webrick/httpauth/authenticator.rb create mode 100644 tool/lib/webrick/httpauth/basicauth.rb create mode 100644 tool/lib/webrick/httpauth/digestauth.rb create mode 100644 tool/lib/webrick/httpauth/htdigest.rb create mode 100644 tool/lib/webrick/httpauth/htgroup.rb create mode 100644 tool/lib/webrick/httpauth/htpasswd.rb create mode 100644 tool/lib/webrick/httpauth/userdb.rb create mode 100644 tool/lib/webrick/httpproxy.rb create mode 100644 tool/lib/webrick/httprequest.rb create mode 100644 tool/lib/webrick/httpresponse.rb create mode 100644 tool/lib/webrick/https.rb create mode 100644 tool/lib/webrick/httpserver.rb create mode 100644 tool/lib/webrick/httpservlet.rb create mode 100644 tool/lib/webrick/httpservlet/abstract.rb create mode 100644 tool/lib/webrick/httpservlet/cgi_runner.rb create mode 100644 tool/lib/webrick/httpservlet/cgihandler.rb create mode 100644 tool/lib/webrick/httpservlet/erbhandler.rb create mode 100644 tool/lib/webrick/httpservlet/filehandler.rb create mode 100644 tool/lib/webrick/httpservlet/prochandler.rb create mode 100644 tool/lib/webrick/httpstatus.rb create mode 100644 tool/lib/webrick/httputils.rb create mode 100644 tool/lib/webrick/httpversion.rb create mode 100644 tool/lib/webrick/log.rb create mode 100644 tool/lib/webrick/server.rb create mode 100644 tool/lib/webrick/ssl.rb create mode 100644 tool/lib/webrick/utils.rb create mode 100644 tool/lib/webrick/version.rb create mode 100644 tool/lib/webrick/webrick.gemspec create mode 100644 tool/test/webrick/.htaccess create mode 100644 tool/test/webrick/test_cgi.rb create mode 100644 tool/test/webrick/test_config.rb create mode 100644 tool/test/webrick/test_cookie.rb create mode 100644 tool/test/webrick/test_do_not_reverse_lookup.rb create mode 100644 tool/test/webrick/test_filehandler.rb create mode 100644 tool/test/webrick/test_htgroup.rb create mode 100644 tool/test/webrick/test_htmlutils.rb create mode 100644 tool/test/webrick/test_httpauth.rb create mode 100644 tool/test/webrick/test_httpproxy.rb create mode 100644 tool/test/webrick/test_httprequest.rb create mode 100644 tool/test/webrick/test_httpresponse.rb create mode 100644 tool/test/webrick/test_https.rb create mode 100644 tool/test/webrick/test_httpserver.rb create mode 100644 tool/test/webrick/test_httpstatus.rb create mode 100644 tool/test/webrick/test_httputils.rb create mode 100644 tool/test/webrick/test_httpversion.rb create mode 100644 tool/test/webrick/test_server.rb create mode 100644 tool/test/webrick/test_ssl_server.rb create mode 100644 tool/test/webrick/test_utils.rb create mode 100644 tool/test/webrick/utils.rb create mode 100644 tool/test/webrick/webrick.cgi create mode 100644 tool/test/webrick/webrick.rhtml create mode 100644 tool/test/webrick/webrick_long_filename.cgi diff --git a/lib/webrick.rb b/lib/webrick.rb deleted file mode 100644 index b854b68db4..0000000000 --- a/lib/webrick.rb +++ /dev/null @@ -1,232 +0,0 @@ -# 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' diff --git a/lib/webrick/.document b/lib/webrick/.document deleted file mode 100644 index c62f89083b..0000000000 --- a/lib/webrick/.document +++ /dev/null @@ -1,6 +0,0 @@ -# Add files to this as they become documented - -*.rb - -httpauth -httpservlet diff --git a/lib/webrick/accesslog.rb b/lib/webrick/accesslog.rb deleted file mode 100644 index e4849637f3..0000000000 --- a/lib/webrick/accesslog.rb +++ /dev/null @@ -1,157 +0,0 @@ -# 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 diff --git a/lib/webrick/cgi.rb b/lib/webrick/cgi.rb deleted file mode 100644 index bb0ae2fc84..0000000000 --- a/lib/webrick/cgi.rb +++ /dev/null @@ -1,313 +0,0 @@ -# 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 diff --git a/lib/webrick/compat.rb b/lib/webrick/compat.rb deleted file mode 100644 index c497a1933c..0000000000 --- a/lib/webrick/compat.rb +++ /dev/null @@ -1,36 +0,0 @@ -# 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 diff --git a/lib/webrick/config.rb b/lib/webrick/config.rb deleted file mode 100644 index 9f2ab44f49..0000000000 --- a/lib/webrick/config.rb +++ /dev/null @@ -1,158 +0,0 @@ -# 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 diff --git a/lib/webrick/cookie.rb b/lib/webrick/cookie.rb deleted file mode 100644 index 5fd3bfb228..0000000000 --- a/lib/webrick/cookie.rb +++ /dev/null @@ -1,172 +0,0 @@ -# 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 diff --git a/lib/webrick/htmlutils.rb b/lib/webrick/htmlutils.rb deleted file mode 100644 index ed9f4ac0d3..0000000000 --- a/lib/webrick/htmlutils.rb +++ /dev/null @@ -1,30 +0,0 @@ -# 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 diff --git a/lib/webrick/httpauth/authenticator.rb b/lib/webrick/httpauth/authenticator.rb deleted file mode 100644 index 8f0eaa3aca..0000000000 --- a/lib/webrick/httpauth/authenticator.rb +++ /dev/null @@ -1,117 +0,0 @@ -# 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 diff --git a/lib/webrick/httpauth/basicauth.rb b/lib/webrick/httpauth/basicauth.rb deleted file mode 100644 index 7d0a9cfc8f..0000000000 --- a/lib/webrick/httpauth/basicauth.rb +++ /dev/null @@ -1,116 +0,0 @@ -# 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 diff --git a/lib/webrick/httpauth/digestauth.rb b/lib/webrick/httpauth/digestauth.rb deleted file mode 100644 index 3cf12899d2..0000000000 --- a/lib/webrick/httpauth/digestauth.rb +++ /dev/null @@ -1,395 +0,0 @@ -# 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 diff --git a/lib/webrick/httpauth/htdigest.rb b/lib/webrick/httpauth/htdigest.rb deleted file mode 100644 index 93b18e2c75..0000000000 --- a/lib/webrick/httpauth/htdigest.rb +++ /dev/null @@ -1,132 +0,0 @@ -# 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 diff --git a/lib/webrick/httpauth/htgroup.rb b/lib/webrick/httpauth/htgroup.rb deleted file mode 100644 index e06c441b18..0000000000 --- a/lib/webrick/httpauth/htgroup.rb +++ /dev/null @@ -1,97 +0,0 @@ -# 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 diff --git a/lib/webrick/httpauth/htpasswd.rb b/lib/webrick/httpauth/htpasswd.rb deleted file mode 100644 index abca30532e..0000000000 --- a/lib/webrick/httpauth/htpasswd.rb +++ /dev/null @@ -1,158 +0,0 @@ -# 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 diff --git a/lib/webrick/httpauth/userdb.rb b/lib/webrick/httpauth/userdb.rb deleted file mode 100644 index 7a17715cdf..0000000000 --- a/lib/webrick/httpauth/userdb.rb +++ /dev/null @@ -1,53 +0,0 @@ -# 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 diff --git a/lib/webrick/httpproxy.rb b/lib/webrick/httpproxy.rb deleted file mode 100644 index 7607c3df88..0000000000 --- a/lib/webrick/httpproxy.rb +++ /dev/null @@ -1,354 +0,0 @@ -# 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 diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb deleted file mode 100644 index d34eac7ecf..0000000000 --- a/lib/webrick/httprequest.rb +++ /dev/null @@ -1,636 +0,0 @@ -# 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 diff --git a/lib/webrick/httpresponse.rb b/lib/webrick/httpresponse.rb deleted file mode 100644 index ba4494ab74..0000000000 --- a/lib/webrick/httpresponse.rb +++ /dev/null @@ -1,564 +0,0 @@ -# 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 diff --git a/lib/webrick/https.rb b/lib/webrick/https.rb deleted file mode 100644 index b0a49bc40b..0000000000 --- a/lib/webrick/https.rb +++ /dev/null @@ -1,152 +0,0 @@ -# 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 diff --git a/lib/webrick/httpserver.rb b/lib/webrick/httpserver.rb deleted file mode 100644 index e85d059319..0000000000 --- a/lib/webrick/httpserver.rb +++ /dev/null @@ -1,294 +0,0 @@ -# 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 diff --git a/lib/webrick/httpservlet.rb b/lib/webrick/httpservlet.rb deleted file mode 100644 index da49a1405b..0000000000 --- a/lib/webrick/httpservlet.rb +++ /dev/null @@ -1,23 +0,0 @@ -# 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 diff --git a/lib/webrick/httpservlet/abstract.rb b/lib/webrick/httpservlet/abstract.rb deleted file mode 100644 index bccb091861..0000000000 --- a/lib/webrick/httpservlet/abstract.rb +++ /dev/null @@ -1,152 +0,0 @@ -# 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 diff --git a/lib/webrick/httpservlet/cgi_runner.rb b/lib/webrick/httpservlet/cgi_runner.rb deleted file mode 100644 index 0398c16749..0000000000 --- a/lib/webrick/httpservlet/cgi_runner.rb +++ /dev/null @@ -1,47 +0,0 @@ -# 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"] diff --git a/lib/webrick/httpservlet/cgihandler.rb b/lib/webrick/httpservlet/cgihandler.rb deleted file mode 100644 index 4457770b7a..0000000000 --- a/lib/webrick/httpservlet/cgihandler.rb +++ /dev/null @@ -1,126 +0,0 @@ -# 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 diff --git a/lib/webrick/httpservlet/erbhandler.rb b/lib/webrick/httpservlet/erbhandler.rb deleted file mode 100644 index cd09e5f216..0000000000 --- a/lib/webrick/httpservlet/erbhandler.rb +++ /dev/null @@ -1,88 +0,0 @@ -# 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 diff --git a/lib/webrick/httpservlet/filehandler.rb b/lib/webrick/httpservlet/filehandler.rb deleted file mode 100644 index 010df0e918..0000000000 --- a/lib/webrick/httpservlet/filehandler.rb +++ /dev/null @@ -1,552 +0,0 @@ -# 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 diff --git a/lib/webrick/httpservlet/prochandler.rb b/lib/webrick/httpservlet/prochandler.rb deleted file mode 100644 index 599ffc4340..0000000000 --- a/lib/webrick/httpservlet/prochandler.rb +++ /dev/null @@ -1,47 +0,0 @@ -# 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 diff --git a/lib/webrick/httpstatus.rb b/lib/webrick/httpstatus.rb deleted file mode 100644 index c811f21964..0000000000 --- a/lib/webrick/httpstatus.rb +++ /dev/null @@ -1,194 +0,0 @@ -# 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 diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb deleted file mode 100644 index f1b9ddf9f0..0000000000 --- a/lib/webrick/httputils.rb +++ /dev/null @@ -1,512 +0,0 @@ -# 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 diff --git a/lib/webrick/httpversion.rb b/lib/webrick/httpversion.rb deleted file mode 100644 index 8a251944a2..0000000000 --- a/lib/webrick/httpversion.rb +++ /dev/null @@ -1,76 +0,0 @@ -# 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 diff --git a/lib/webrick/log.rb b/lib/webrick/log.rb deleted file mode 100644 index 2c1fdfe602..0000000000 --- a/lib/webrick/log.rb +++ /dev/null @@ -1,156 +0,0 @@ -# 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 diff --git a/lib/webrick/server.rb b/lib/webrick/server.rb deleted file mode 100644 index fd6b7a61b5..0000000000 --- a/lib/webrick/server.rb +++ /dev/null @@ -1,381 +0,0 @@ -# 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 diff --git a/lib/webrick/ssl.rb b/lib/webrick/ssl.rb deleted file mode 100644 index e448095a12..0000000000 --- a/lib/webrick/ssl.rb +++ /dev/null @@ -1,215 +0,0 @@ -# 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 diff --git a/lib/webrick/utils.rb b/lib/webrick/utils.rb deleted file mode 100644 index a96d6f03fd..0000000000 --- a/lib/webrick/utils.rb +++ /dev/null @@ -1,265 +0,0 @@ -# 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 diff --git a/lib/webrick/version.rb b/lib/webrick/version.rb deleted file mode 100644 index 5a21f82d93..0000000000 --- a/lib/webrick/version.rb +++ /dev/null @@ -1,18 +0,0 @@ -# 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.6.0" -end diff --git a/lib/webrick/webrick.gemspec b/lib/webrick/webrick.gemspec deleted file mode 100644 index 5ede24d7ec..0000000000 --- a/lib/webrick/webrick.gemspec +++ /dev/null @@ -1,76 +0,0 @@ -# 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://www.ruby-lang.org" - s.licenses = ["Ruby", "BSD-2-Clause"] - - if s.respond_to?(:metadata=) - s.metadata = { - "bug_tracker_uri" => "https://bugs.ruby-lang.org/projects/ruby-master/issues", - "homepage_uri" => "https://www.ruby-lang.org", - "source_code_uri" => "https://git.ruby-lang.org/ruby.git/" - } - end - - s.add_development_dependency "rake" -end diff --git a/test/webrick/.htaccess b/test/webrick/.htaccess deleted file mode 100644 index 69d4659b9f..0000000000 --- a/test/webrick/.htaccess +++ /dev/null @@ -1 +0,0 @@ -this file should not be published. diff --git a/test/webrick/test_cgi.rb b/test/webrick/test_cgi.rb deleted file mode 100644 index 7a75cf565e..0000000000 --- a/test/webrick/test_cgi.rb +++ /dev/null @@ -1,170 +0,0 @@ -# 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 diff --git a/test/webrick/test_config.rb b/test/webrick/test_config.rb deleted file mode 100644 index a54a667452..0000000000 --- a/test/webrick/test_config.rb +++ /dev/null @@ -1,17 +0,0 @@ -# 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 diff --git a/test/webrick/test_cookie.rb b/test/webrick/test_cookie.rb deleted file mode 100644 index e46185f127..0000000000 --- a/test/webrick/test_cookie.rb +++ /dev/null @@ -1,141 +0,0 @@ -# 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 diff --git a/test/webrick/test_do_not_reverse_lookup.rb b/test/webrick/test_do_not_reverse_lookup.rb deleted file mode 100644 index efcb5a9299..0000000000 --- a/test/webrick/test_do_not_reverse_lookup.rb +++ /dev/null @@ -1,71 +0,0 @@ -# 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 diff --git a/test/webrick/test_filehandler.rb b/test/webrick/test_filehandler.rb deleted file mode 100644 index 998e03f690..0000000000 --- a/test/webrick/test_filehandler.rb +++ /dev/null @@ -1,402 +0,0 @@ -# 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 diff --git a/test/webrick/test_htgroup.rb b/test/webrick/test_htgroup.rb deleted file mode 100644 index 8749711df5..0000000000 --- a/test/webrick/test_htgroup.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "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 diff --git a/test/webrick/test_htmlutils.rb b/test/webrick/test_htmlutils.rb deleted file mode 100644 index ae1b8efa95..0000000000 --- a/test/webrick/test_htmlutils.rb +++ /dev/null @@ -1,21 +0,0 @@ -# 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 diff --git a/test/webrick/test_httpproxy.rb b/test/webrick/test_httpproxy.rb deleted file mode 100644 index 1c2f2fce52..0000000000 --- a/test/webrick/test_httpproxy.rb +++ /dev/null @@ -1,466 +0,0 @@ -# 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 diff --git a/test/webrick/test_httprequest.rb b/test/webrick/test_httprequest.rb deleted file mode 100644 index 759ccbdada..0000000000 --- a/test/webrick/test_httprequest.rb +++ /dev/null @@ -1,488 +0,0 @@ -# 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 diff --git a/test/webrick/test_httpresponse.rb b/test/webrick/test_httpresponse.rb deleted file mode 100644 index 89a0f7036e..0000000000 --- a/test/webrick/test_httpresponse.rb +++ /dev/null @@ -1,282 +0,0 @@ -# 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 diff --git a/test/webrick/test_httpserver.rb b/test/webrick/test_httpserver.rb deleted file mode 100644 index 4133be85ad..0000000000 --- a/test/webrick/test_httpserver.rb +++ /dev/null @@ -1,543 +0,0 @@ -# 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 diff --git a/test/webrick/test_httpstatus.rb b/test/webrick/test_httpstatus.rb deleted file mode 100644 index fd0570d5c6..0000000000 --- a/test/webrick/test_httpstatus.rb +++ /dev/null @@ -1,35 +0,0 @@ -# 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 diff --git a/test/webrick/test_httputils.rb b/test/webrick/test_httputils.rb deleted file mode 100644 index 00f297bd09..0000000000 --- a/test/webrick/test_httputils.rb +++ /dev/null @@ -1,101 +0,0 @@ -# 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 diff --git a/test/webrick/test_httpversion.rb b/test/webrick/test_httpversion.rb deleted file mode 100644 index e50ee17971..0000000000 --- a/test/webrick/test_httpversion.rb +++ /dev/null @@ -1,41 +0,0 @@ -# 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 diff --git a/test/webrick/test_server.rb b/test/webrick/test_server.rb deleted file mode 100644 index 815cc3ce39..0000000000 --- a/test/webrick/test_server.rb +++ /dev/null @@ -1,191 +0,0 @@ -# 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 diff --git a/test/webrick/test_ssl_server.rb b/test/webrick/test_ssl_server.rb deleted file mode 100644 index 4e52598bf5..0000000000 --- a/test/webrick/test_ssl_server.rb +++ /dev/null @@ -1,67 +0,0 @@ -require "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 diff --git a/test/webrick/test_utils.rb b/test/webrick/test_utils.rb deleted file mode 100644 index c2b7a36e8a..0000000000 --- a/test/webrick/test_utils.rb +++ /dev/null @@ -1,110 +0,0 @@ -# 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 diff --git a/test/webrick/utils.rb b/test/webrick/utils.rb deleted file mode 100644 index 56d3a30ea4..0000000000 --- a/test/webrick/utils.rb +++ /dev/null @@ -1,82 +0,0 @@ -# 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 diff --git a/test/webrick/webrick.cgi b/test/webrick/webrick.cgi deleted file mode 100644 index a294fa72f9..0000000000 --- a/test/webrick/webrick.cgi +++ /dev/null @@ -1,38 +0,0 @@ -#!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 diff --git a/test/webrick/webrick.rhtml b/test/webrick/webrick.rhtml deleted file mode 100644 index a7bbe43fb5..0000000000 --- a/test/webrick/webrick.rhtml +++ /dev/null @@ -1,4 +0,0 @@ -req to <%= -servlet_request.request_uri -%> <%= -servlet_request.query.inspect %> diff --git a/test/webrick/webrick_long_filename.cgi b/test/webrick/webrick_long_filename.cgi deleted file mode 100644 index 43c1af825c..0000000000 --- a/test/webrick/webrick_long_filename.cgi +++ /dev/null @@ -1,36 +0,0 @@ -#!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 diff --git a/tool/lib/webrick.rb b/tool/lib/webrick.rb new file mode 100644 index 0000000000..b854b68db4 --- /dev/null +++ b/tool/lib/webrick.rb @@ -0,0 +1,232 @@ +# 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' diff --git a/tool/lib/webrick/.document b/tool/lib/webrick/.document new file mode 100644 index 0000000000..c62f89083b --- /dev/null +++ b/tool/lib/webrick/.document @@ -0,0 +1,6 @@ +# Add files to this as they become documented + +*.rb + +httpauth +httpservlet diff --git a/tool/lib/webrick/accesslog.rb b/tool/lib/webrick/accesslog.rb new file mode 100644 index 0000000000..e4849637f3 --- /dev/null +++ b/tool/lib/webrick/accesslog.rb @@ -0,0 +1,157 @@ +# 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 diff --git a/tool/lib/webrick/cgi.rb b/tool/lib/webrick/cgi.rb new file mode 100644 index 0000000000..bb0ae2fc84 --- /dev/null +++ b/tool/lib/webrick/cgi.rb @@ -0,0 +1,313 @@ +# 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 diff --git a/tool/lib/webrick/compat.rb b/tool/lib/webrick/compat.rb new file mode 100644 index 0000000000..c497a1933c --- /dev/null +++ b/tool/lib/webrick/compat.rb @@ -0,0 +1,36 @@ +# 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 diff --git a/tool/lib/webrick/config.rb b/tool/lib/webrick/config.rb new file mode 100644 index 0000000000..9f2ab44f49 --- /dev/null +++ b/tool/lib/webrick/config.rb @@ -0,0 +1,158 @@ +# 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 diff --git a/tool/lib/webrick/cookie.rb b/tool/lib/webrick/cookie.rb new file mode 100644 index 0000000000..5fd3bfb228 --- /dev/null +++ b/tool/lib/webrick/cookie.rb @@ -0,0 +1,172 @@ +# 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 diff --git a/tool/lib/webrick/htmlutils.rb b/tool/lib/webrick/htmlutils.rb new file mode 100644 index 0000000000..ed9f4ac0d3 --- /dev/null +++ b/tool/lib/webrick/htmlutils.rb @@ -0,0 +1,30 @@ +# 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 diff --git a/tool/lib/webrick/httpauth/authenticator.rb b/tool/lib/webrick/httpauth/authenticator.rb new file mode 100644 index 0000000000..8f0eaa3aca --- /dev/null +++ b/tool/lib/webrick/httpauth/authenticator.rb @@ -0,0 +1,117 @@ +# 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 diff --git a/tool/lib/webrick/httpauth/basicauth.rb b/tool/lib/webrick/httpauth/basicauth.rb new file mode 100644 index 0000000000..7d0a9cfc8f --- /dev/null +++ b/tool/lib/webrick/httpauth/basicauth.rb @@ -0,0 +1,116 @@ +# 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 diff --git a/tool/lib/webrick/httpauth/digestauth.rb b/tool/lib/webrick/httpauth/digestauth.rb new file mode 100644 index 0000000000..3cf12899d2 --- /dev/null +++ b/tool/lib/webrick/httpauth/digestauth.rb @@ -0,0 +1,395 @@ +# 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 diff --git a/tool/lib/webrick/httpauth/htdigest.rb b/tool/lib/webrick/httpauth/htdigest.rb new file mode 100644 index 0000000000..93b18e2c75 --- /dev/null +++ b/tool/lib/webrick/httpauth/htdigest.rb @@ -0,0 +1,132 @@ +# 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 diff --git a/tool/lib/webrick/httpauth/htgroup.rb b/tool/lib/webrick/httpauth/htgroup.rb new file mode 100644 index 0000000000..e06c441b18 --- /dev/null +++ b/tool/lib/webrick/httpauth/htgroup.rb @@ -0,0 +1,97 @@ +# 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 diff --git a/tool/lib/webrick/httpauth/htpasswd.rb b/tool/lib/webrick/httpauth/htpasswd.rb new file mode 100644 index 0000000000..abca30532e --- /dev/null +++ b/tool/lib/webrick/httpauth/htpasswd.rb @@ -0,0 +1,158 @@ +# 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 diff --git a/tool/lib/webrick/httpauth/userdb.rb b/tool/lib/webrick/httpauth/userdb.rb new file mode 100644 index 0000000000..7a17715cdf --- /dev/null +++ b/tool/lib/webrick/httpauth/userdb.rb @@ -0,0 +1,53 @@ +# 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 diff --git a/tool/lib/webrick/httpproxy.rb b/tool/lib/webrick/httpproxy.rb new file mode 100644 index 0000000000..7607c3df88 --- /dev/null +++ b/tool/lib/webrick/httpproxy.rb @@ -0,0 +1,354 @@ +# 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 diff --git a/tool/lib/webrick/httprequest.rb b/tool/lib/webrick/httprequest.rb new file mode 100644 index 0000000000..d34eac7ecf --- /dev/null +++ b/tool/lib/webrick/httprequest.rb @@ -0,0 +1,636 @@ +# 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 diff --git a/tool/lib/webrick/httpresponse.rb b/tool/lib/webrick/httpresponse.rb new file mode 100644 index 0000000000..ba4494ab74 --- /dev/null +++ b/tool/lib/webrick/httpresponse.rb @@ -0,0 +1,564 @@ +# 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 diff --git a/tool/lib/webrick/https.rb b/tool/lib/webrick/https.rb new file mode 100644 index 0000000000..b0a49bc40b --- /dev/null +++ b/tool/lib/webrick/https.rb @@ -0,0 +1,152 @@ +# 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 diff --git a/tool/lib/webrick/httpserver.rb b/tool/lib/webrick/httpserver.rb new file mode 100644 index 0000000000..e85d059319 --- /dev/null +++ b/tool/lib/webrick/httpserver.rb @@ -0,0 +1,294 @@ +# 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 diff --git a/tool/lib/webrick/httpservlet.rb b/tool/lib/webrick/httpservlet.rb new file mode 100644 index 0000000000..da49a1405b --- /dev/null +++ b/tool/lib/webrick/httpservlet.rb @@ -0,0 +1,23 @@ +# 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 diff --git a/tool/lib/webrick/httpservlet/abstract.rb b/tool/lib/webrick/httpservlet/abstract.rb new file mode 100644 index 0000000000..bccb091861 --- /dev/null +++ b/tool/lib/webrick/httpservlet/abstract.rb @@ -0,0 +1,152 @@ +# 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 diff --git a/tool/lib/webrick/httpservlet/cgi_runner.rb b/tool/lib/webrick/httpservlet/cgi_runner.rb new file mode 100644 index 0000000000..0398c16749 --- /dev/null +++ b/tool/lib/webrick/httpservlet/cgi_runner.rb @@ -0,0 +1,47 @@ +# 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"] diff --git a/tool/lib/webrick/httpservlet/cgihandler.rb b/tool/lib/webrick/httpservlet/cgihandler.rb new file mode 100644 index 0000000000..4457770b7a --- /dev/null +++ b/tool/lib/webrick/httpservlet/cgihandler.rb @@ -0,0 +1,126 @@ +# 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 diff --git a/tool/lib/webrick/httpservlet/erbhandler.rb b/tool/lib/webrick/httpservlet/erbhandler.rb new file mode 100644 index 0000000000..cd09e5f216 --- /dev/null +++ b/tool/lib/webrick/httpservlet/erbhandler.rb @@ -0,0 +1,88 @@ +# 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 diff --git a/tool/lib/webrick/httpservlet/filehandler.rb b/tool/lib/webrick/httpservlet/filehandler.rb new file mode 100644 index 0000000000..010df0e918 --- /dev/null +++ b/tool/lib/webrick/httpservlet/filehandler.rb @@ -0,0 +1,552 @@ +# 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 diff --git a/tool/lib/webrick/httpservlet/prochandler.rb b/tool/lib/webrick/httpservlet/prochandler.rb new file mode 100644 index 0000000000..599ffc4340 --- /dev/null +++ b/tool/lib/webrick/httpservlet/prochandler.rb @@ -0,0 +1,47 @@ +# 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 diff --git a/tool/lib/webrick/httpstatus.rb b/tool/lib/webrick/httpstatus.rb new file mode 100644 index 0000000000..c811f21964 --- /dev/null +++ b/tool/lib/webrick/httpstatus.rb @@ -0,0 +1,194 @@ +# 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 diff --git a/tool/lib/webrick/httputils.rb b/tool/lib/webrick/httputils.rb new file mode 100644 index 0000000000..f1b9ddf9f0 --- /dev/null +++ b/tool/lib/webrick/httputils.rb @@ -0,0 +1,512 @@ +# 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 diff --git a/tool/lib/webrick/httpversion.rb b/tool/lib/webrick/httpversion.rb new file mode 100644 index 0000000000..8a251944a2 --- /dev/null +++ b/tool/lib/webrick/httpversion.rb @@ -0,0 +1,76 @@ +# 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 diff --git a/tool/lib/webrick/log.rb b/tool/lib/webrick/log.rb new file mode 100644 index 0000000000..2c1fdfe602 --- /dev/null +++ b/tool/lib/webrick/log.rb @@ -0,0 +1,156 @@ +# 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 diff --git a/tool/lib/webrick/server.rb b/tool/lib/webrick/server.rb new file mode 100644 index 0000000000..fd6b7a61b5 --- /dev/null +++ b/tool/lib/webrick/server.rb @@ -0,0 +1,381 @@ +# 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 diff --git a/tool/lib/webrick/ssl.rb b/tool/lib/webrick/ssl.rb new file mode 100644 index 0000000000..e448095a12 --- /dev/null +++ b/tool/lib/webrick/ssl.rb @@ -0,0 +1,215 @@ +# 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 diff --git a/tool/lib/webrick/utils.rb b/tool/lib/webrick/utils.rb new file mode 100644 index 0000000000..a96d6f03fd --- /dev/null +++ b/tool/lib/webrick/utils.rb @@ -0,0 +1,265 @@ +# 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 diff --git a/tool/lib/webrick/version.rb b/tool/lib/webrick/version.rb new file mode 100644 index 0000000000..5a21f82d93 --- /dev/null +++ b/tool/lib/webrick/version.rb @@ -0,0 +1,18 @@ +# 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.6.0" +end diff --git a/tool/lib/webrick/webrick.gemspec b/tool/lib/webrick/webrick.gemspec new file mode 100644 index 0000000000..5ede24d7ec --- /dev/null +++ b/tool/lib/webrick/webrick.gemspec @@ -0,0 +1,76 @@ +# 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://www.ruby-lang.org" + s.licenses = ["Ruby", "BSD-2-Clause"] + + if s.respond_to?(:metadata=) + s.metadata = { + "bug_tracker_uri" => "https://bugs.ruby-lang.org/projects/ruby-master/issues", + "homepage_uri" => "https://www.ruby-lang.org", + "source_code_uri" => "https://git.ruby-lang.org/ruby.git/" + } + end + + s.add_development_dependency "rake" +end diff --git a/tool/test/webrick/.htaccess b/tool/test/webrick/.htaccess new file mode 100644 index 0000000000..69d4659b9f --- /dev/null +++ b/tool/test/webrick/.htaccess @@ -0,0 +1 @@ +this file should not be published. diff --git a/tool/test/webrick/test_cgi.rb b/tool/test/webrick/test_cgi.rb new file mode 100644 index 0000000000..7a75cf565e --- /dev/null +++ b/tool/test/webrick/test_cgi.rb @@ -0,0 +1,170 @@ +# 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 diff --git a/tool/test/webrick/test_config.rb b/tool/test/webrick/test_config.rb new file mode 100644 index 0000000000..a54a667452 --- /dev/null +++ b/tool/test/webrick/test_config.rb @@ -0,0 +1,17 @@ +# 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 diff --git a/tool/test/webrick/test_cookie.rb b/tool/test/webrick/test_cookie.rb new file mode 100644 index 0000000000..e46185f127 --- /dev/null +++ b/tool/test/webrick/test_cookie.rb @@ -0,0 +1,141 @@ +# 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 diff --git a/tool/test/webrick/test_do_not_reverse_lookup.rb b/tool/test/webrick/test_do_not_reverse_lookup.rb new file mode 100644 index 0000000000..efcb5a9299 --- /dev/null +++ b/tool/test/webrick/test_do_not_reverse_lookup.rb @@ -0,0 +1,71 @@ +# 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 diff --git a/tool/test/webrick/test_filehandler.rb b/tool/test/webrick/test_filehandler.rb new file mode 100644 index 0000000000..998e03f690 --- /dev/null +++ b/tool/test/webrick/test_filehandler.rb @@ -0,0 +1,402 @@ +# 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 diff --git a/tool/test/webrick/test_htgroup.rb b/tool/test/webrick/test_htgroup.rb new file mode 100644 index 0000000000..8749711df5 --- /dev/null +++ b/tool/test/webrick/test_htgroup.rb @@ -0,0 +1,19 @@ +require "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 diff --git a/tool/test/webrick/test_htmlutils.rb b/tool/test/webrick/test_htmlutils.rb new file mode 100644 index 0000000000..ae1b8efa95 --- /dev/null +++ b/tool/test/webrick/test_htmlutils.rb @@ -0,0 +1,21 @@ +# 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 diff --git a/tool/test/webrick/test_httpproxy.rb b/tool/test/webrick/test_httpproxy.rb new file mode 100644 index 0000000000..1c2f2fce52 --- /dev/null +++ b/tool/test/webrick/test_httpproxy.rb @@ -0,0 +1,466 @@ +# 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 diff --git a/tool/test/webrick/test_httprequest.rb b/tool/test/webrick/test_httprequest.rb new file mode 100644 index 0000000000..759ccbdada --- /dev/null +++ b/tool/test/webrick/test_httprequest.rb @@ -0,0 +1,488 @@ +# 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 diff --git a/tool/test/webrick/test_httpresponse.rb b/tool/test/webrick/test_httpresponse.rb new file mode 100644 index 0000000000..89a0f7036e --- /dev/null +++ b/tool/test/webrick/test_httpresponse.rb @@ -0,0 +1,282 @@ +# 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 diff --git a/tool/test/webrick/test_httpserver.rb b/tool/test/webrick/test_httpserver.rb new file mode 100644 index 0000000000..4133be85ad --- /dev/null +++ b/tool/test/webrick/test_httpserver.rb @@ -0,0 +1,543 @@ +# 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 diff --git a/tool/test/webrick/test_httpstatus.rb b/tool/test/webrick/test_httpstatus.rb new file mode 100644 index 0000000000..fd0570d5c6 --- /dev/null +++ b/tool/test/webrick/test_httpstatus.rb @@ -0,0 +1,35 @@ +# 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 diff --git a/tool/test/webrick/test_httputils.rb b/tool/test/webrick/test_httputils.rb new file mode 100644 index 0000000000..00f297bd09 --- /dev/null +++ b/tool/test/webrick/test_httputils.rb @@ -0,0 +1,101 @@ +# 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 diff --git a/tool/test/webrick/test_httpversion.rb b/tool/test/webrick/test_httpversion.rb new file mode 100644 index 0000000000..e50ee17971 --- /dev/null +++ b/tool/test/webrick/test_httpversion.rb @@ -0,0 +1,41 @@ +# 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 diff --git a/tool/test/webrick/test_server.rb b/tool/test/webrick/test_server.rb new file mode 100644 index 0000000000..815cc3ce39 --- /dev/null +++ b/tool/test/webrick/test_server.rb @@ -0,0 +1,191 @@ +# 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 diff --git a/tool/test/webrick/test_ssl_server.rb b/tool/test/webrick/test_ssl_server.rb new file mode 100644 index 0000000000..4e52598bf5 --- /dev/null +++ b/tool/test/webrick/test_ssl_server.rb @@ -0,0 +1,67 @@ +require "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 diff --git a/tool/test/webrick/test_utils.rb b/tool/test/webrick/test_utils.rb new file mode 100644 index 0000000000..c2b7a36e8a --- /dev/null +++ b/tool/test/webrick/test_utils.rb @@ -0,0 +1,110 @@ +# 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 diff --git a/tool/test/webrick/utils.rb b/tool/test/webrick/utils.rb new file mode 100644 index 0000000000..56d3a30ea4 --- /dev/null +++ b/tool/test/webrick/utils.rb @@ -0,0 +1,82 @@ +# 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 diff --git a/tool/test/webrick/webrick.cgi b/tool/test/webrick/webrick.cgi new file mode 100644 index 0000000000..a294fa72f9 --- /dev/null +++ b/tool/test/webrick/webrick.cgi @@ -0,0 +1,38 @@ +#!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 diff --git a/tool/test/webrick/webrick.rhtml b/tool/test/webrick/webrick.rhtml new file mode 100644 index 0000000000..a7bbe43fb5 --- /dev/null +++ b/tool/test/webrick/webrick.rhtml @@ -0,0 +1,4 @@ +req to <%= +servlet_request.request_uri +%> <%= +servlet_request.query.inspect %> diff --git a/tool/test/webrick/webrick_long_filename.cgi b/tool/test/webrick/webrick_long_filename.cgi new file mode 100644 index 0000000000..43c1af825c --- /dev/null +++ b/tool/test/webrick/webrick_long_filename.cgi @@ -0,0 +1,36 @@ +#!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 -- cgit v1.2.3