brew vendor-gems: commit updates.

This commit is contained in:
BrewTestBot 2021-02-02 06:05:35 +00:00
parent 5badc08e31
commit 99d8e9af87
38 changed files with 7118 additions and 3 deletions

View File

@ -36,14 +36,15 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/http-cookie-1.0.3/lib
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mime-types-data-3.2020.1104/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mime-types-3.3.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/net-http-digest_auth-1.4.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/net-http-persistent-4.0.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/net-http-persistent-4.0.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mini_portile2-2.5.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/x86_64-darwin-14/2.6.0-static/racc-1.5.2"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/racc-1.5.2/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/nokogiri-1.11.1-x86_64-darwin/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/ntlm-http-0.1.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/webrick-1.7.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/webrobots-0.1.2/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mechanize-2.7.6/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mechanize-2.7.7/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/method_source-1.0.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/mustache-1.1.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parallel-1.20.1/lib"

View File

@ -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'

View File

@ -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 <tt>req.attributes</tt>
# %{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

View File

@ -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 <code>@options</code> 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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, '&amp;')
str.gsub!(/\"/n, '&quot;')
str.gsub!(/>/n, '&gt;')
str.gsub!(/</n, '&lt;')
str.force_encoding(string.encoding)
end
module_function :escape
end
end

View File

@ -0,0 +1,96 @@
# frozen_string_literal: false
#
# httpauth.rb -- HTTP access authentication
#
# 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: httpauth.rb,v 1.14 2003/07/22 19:20:42 gotoyuzo Exp $
require_relative 'httpauth/basicauth'
require_relative 'httpauth/digestauth'
require_relative 'httpauth/htpasswd'
require_relative 'httpauth/htdigest'
require_relative 'httpauth/htgroup'
module WEBrick
##
# HTTPAuth provides both basic and digest authentication.
#
# To enable authentication for requests in WEBrick you will need a user
# database and an authenticator. To start, here's an Htpasswd database for
# use with a DigestAuth authenticator:
#
# config = { :Realm => '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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <code>header['content-length']</code> 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 = "<HTML><A HREF=\"#{url}\">#{url}</A>.</HTML>\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_
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<HTML>
<HEAD><TITLE>#{HTMLUtils::escape(@reason_phrase)}</TITLE></HEAD>
<BODY>
<H1>#{HTMLUtils::escape(@reason_phrase)}</H1>
#{HTMLUtils::escape(ex.message)}
<HR>
_end_of_html_
if backtrace && $DEBUG
@body << "backtrace of `#{HTMLUtils::escape(ex.class.to_s)}' "
@body << "#{HTMLUtils::escape(ex.message)}"
@body << "<PRE>"
ex.backtrace.each{|line| @body << "\t#{line}\n"}
@body << "</PRE><HR>"
end
@body << <<-_end_of_html_
<ADDRESS>
#{HTMLUtils::escape(@config[:ServerSoftware])} at
#{host}:#{port}
</ADDRESS>
</BODY>
</HTML>
_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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = "<p " \
# %q{style="color: #{@color}; font-size: #{@size}"} \
# ">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

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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_
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
<HEAD>
<TITLE>#{title}</TITLE>
<style type="text/css">
<!--
.name, .mtime { text-align: left; }
.size { text-align: right; }
td { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
table { border-collapse: collapse; }
tr th { border-bottom: 2px groove; }
//-->
</style>
</HEAD>
<BODY>
<H1>#{title}</H1>
_end_of_html_
res.body << "<TABLE width=\"100%\"><THEAD><TR>\n"
res.body << "<TH class=\"name\"><A HREF=\"?N=#{d1}#{query}\">Name</A></TH>"
res.body << "<TH class=\"mtime\"><A HREF=\"?M=#{d1}#{query}\">Last modified</A></TH>"
res.body << "<TH class=\"size\"><A HREF=\"?S=#{d1}#{query}\">Size</A></TH>\n"
res.body << "</TR></THEAD>\n"
res.body << "<TBODY>\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 = "<TR><TD class=\"name\"><A HREF=\"#{HTTPUtils::escape(name)}#{query if name.end_with?('/')}\">#{HTMLUtils::escape(dname)}</A></TD>"
s << "<TD class=\"mtime\">" << (time ? time.strftime("%Y/%m/%d %H:%M") : "") << "</TD>"
s << "<TD class=\"size\">" << (size >= 0 ? size.to_s : "-") << "</TD></TR>\n"
res.body << s
}
res.body << "</TBODY></TABLE>"
res.body << "<HR>"
res.body << <<-_end_of_html_
<ADDRESS>
#{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
at #{req.host}:#{req.port}
</ADDRESS>
</BODY>
</HTML>
_end_of_html_
end
# :startdoc:
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <tt>"[%Y-%m-%d %H:%M:%S]"</tt>
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

View File

@ -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
# <code>:MaxClients</code> 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 <code>:WEBrickSocket</code> 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: <address unknown>"
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: <address unknown>"
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

View File

@ -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
# <code>:SSLCertName</code> 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

View File

@ -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

View File

@ -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.7.0"
end