Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+10
View File
@@ -0,0 +1,10 @@
source 'https://rubygems.org'
gemspec
gem "rake"
gem "test-unit"
gem "test-unit-ruby-core"
# rbs requires ruby 3.0+
gem "rbs", require: false if !RUBY_VERSION.start_with?('2.')
+22
View File
@@ -0,0 +1,22 @@
Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
+63
View File
@@ -0,0 +1,63 @@
# Webrick
WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server.
WEBrick features complete logging of both server operations and HTTP access.
WEBrick supports both basic and digest authentication in addition to algorithms not in RFC 2617.
A WEBrick server can be composed of multiple WEBrick servers or servlets to provide differing behavior on a per-host or per-path basis. WEBrick includes servlets for handling CGI scripts, ERB pages, Ruby blocks and directory listings.
WEBrick also includes tools for daemonizing a process and starting a process at a higher privilege level and dropping permissions.
WEBrick is suitable for use in testing and for development. However, while the developers of WEBrick will attempt to fix security issues, they do not encourage the use of WEBrick to serve production web applications that may be subject to hostile input.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'webrick'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install webrick
## Usage
To create a new WEBrick::HTTPServer that will listen to connections on port 8000 and serve documents from the current user's public_html folder:
```ruby
require 'webrick'
root = File.expand_path '~/public_html'
server = WEBrick::HTTPServer.new :Port => 8000, :DocumentRoot => root
```
To run the server you will need to provide a suitable shutdown hook as
starting the server blocks the current thread:
```ruby
trap 'INT' do server.shutdown end
server.start
```
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/webrick.
## License
The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause).
+10
View File
@@ -0,0 +1,10 @@
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test/lib"
t.ruby_opts << "-rhelper"
t.test_files = FileList["test/**/test_*.rb"]
end
task :default => :test
+232
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'
@@ -0,0 +1,157 @@
# frozen_string_literal: true
#--
# 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
+313
View File
@@ -0,0 +1,313 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,36 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,158 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,172 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,30 @@
# frozen_string_literal: true
#--
# 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
@@ -0,0 +1,96 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,117 @@
# frozen_string_literal: true
#--
# 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:
ResponseInfoField = "Proxy-Authentication-Info" # :nodoc:
AuthException = HTTPStatus::ProxyAuthenticationRequired # :nodoc:
end
end
end
@@ -0,0 +1,116 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,395 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,132 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,97 @@
# frozen_string_literal: true
#
# 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*/, 2)
@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
@@ -0,0 +1,158 @@
# frozen_string_literal: true
#
# 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(":", 2)
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(":", 2)
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
@@ -0,0 +1,53 @@
# frozen_string_literal: true
#--
# 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
@@ -0,0 +1,354 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,668 @@
# frozen_string_literal: true
#
# 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
@raw_header = Array.new
@header = nil
@cookies = []
@accept = []
@accept_charset = []
@accept_encoding = []
@accept_language = []
@body = +""
@addr = []
@peeraddr = []
@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:
# https://www.rfc-editor.org/rfc/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++)(?: 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
end_of_headers = false
while line = read_line(socket)
if line == CRLF
end_of_headers = true
break
end
if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH
raise HTTPStatus::RequestEntityTooLarge, 'headers too large'
end
if line.include?("\x00")
raise HTTPStatus::BadRequest, 'null byte in header'
end
@raw_header << line
end
# Allow if @header already set to support chunked trailers
raise HTTPStatus::EOFError unless end_of_headers || @header
end
@header = HTTPUtils::parse_header(@raw_header.join)
if (content_length = @header['content-length']) && content_length.length != 0
if content_length.length > 1
raise HTTPStatus::BadRequest, "multiple content-length request headers"
elsif !/\A\d+\z/.match?(content_length[0])
raise HTTPStatus::BadRequest, "invalid content-length request header"
end
end
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"]
host, port = parse_host_request_line(self["host"])
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
host_pattern = URI::RFC2396_Parser.new.pattern.fetch(:HOST)
HOST_PATTERN = /\A(#{host_pattern})(?::(\d+))?\z/n
def parse_host_request_line(host)
host.scan(HOST_PATTERN)[0]
end
def read_body(socket, block)
return unless socket
if tc = self['transfer-encoding']
if self['content-length']
raise HTTPStatus::BadRequest, "request with both transfer-encoding and content-length, possible request smuggling"
end
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)
raise HTTPStatus::LengthRequired
end
return @body
end
def read_chunk_size(socket)
line = read_line(socket)
if /\A([0-9a-fA-F]+)(?:;(\S+(?:=\S+)?))?\r\n\z/ =~ 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
line = read_line(socket) # skip CRLF
unless line == "\r\n"
raise HTTPStatus::BadRequest, "extra data after chunk '#{line}'."
end
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
@@ -0,0 +1,588 @@
# frozen_string_literal: true
#
# 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
##
# Set the response body proc as an streaming/upgrade response.
attr_accessor :upgrade
##
# 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
##
# Sets the response to be a streaming/upgrade response.
# This will disable keep-alive and chunked transfer encoding.
def upgrade!(protocol)
@upgrade = protocol
@keep_alive = false
@chunked = false
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
if @upgrade
@header['connection'] = 'upgrade'
@header['upgrade'] = @upgrade
@keep_alive = false
return
end
# 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?(:bytesize)
@header['content-length'] = @body.bytesize.to_s
else
@header['connection'] = 'close'
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().dup
@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
if @bodytempfile
@bodytempfile.rewind
IO.copy_stream(@bodytempfile, socket)
else
@body.call(socket)
end
if content_length = @header['content-length']
@sent_size = content_length.to_i
end
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
+152
View File
@@ -0,0 +1,152 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,294 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,23 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,152 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,47 @@
# frozen_string_literal: true
#
# 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"]
@@ -0,0 +1,126 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,88 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,552 @@
# frozen_string_literal: true
#
# 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}")}.dup
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
@@ -0,0 +1,48 @@
# frozen_string_literal: true
#
# 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
alias do_PUT do_GET
# :startdoc:
end
end
end
@@ -0,0 +1,194 @@
# frozen_string_literal: true
#--
# 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
@@ -0,0 +1,543 @@
# frozen_string_literal: true
#
# 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",
"avif" => "image/avif",
"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",
"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",
"ico" => "image/x-icon",
"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",
"mp4" => "video/mp4",
"mpe" => "video/mpeg",
"mpeg" => "video/mpeg",
"mpg" => "video/mpeg",
"otf" => "font/otf",
"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",
"ttc" => "font/collection",
"ttf" => "font/ttf",
"txt" => "text/plain",
"wasm" => "application/wasm",
"webm" => "video/webm",
"webmanifest" => "application/manifest+json",
"webp" => "image/webp",
"woff" => "font/woff",
"woff2" => "font/woff2",
"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.
File.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.
class SplitHeader < Array
def join(separator = ", ")
super
end
end
class CookieHeader < Array
def join(separator = "; ")
super
end
end
HEADER_CLASSES = Hash.new(SplitHeader).update({
"cookie" => CookieHeader,
})
def parse_header(raw)
header = Hash.new([].freeze)
field = nil
raw.each_line{|line|
case line
when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):([^\r\n\0]*?)\r\n\z/om
field, value = $1, $2
field.downcase!
header[field] = HEADER_CLASSES[field].new unless header.has_key?(field)
header[field] << value
when /^[ \t]+([^\r\n\0]*?)\r\n/om
unless field
raise HTTPStatus::BadRequest, "bad header '#{line}'."
end
value = line
value.gsub!(/\A[ \t]+/, '')
value.slice!(-2..-1)
header[field][-1] << " " << value
else
raise HTTPStatus::BadRequest, "bad header '#{line}'."
end
}
header.each{|key, values|
values.each{|value|
value.gsub!(/\A[ \t]+/, '')
value.gsub!(/[ \t]+\z/, '')
}
}
header
end
module_function :parse_header
##
# Splits a header value +str+ according to HTTP specification.
def split_header_value(str)
str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]++)+)
(?:,[ \t]*|\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(/,[ \t]*/)
parts.each {|part|
if m = %r{^([^ \t,]+?)(?:;[ \t]*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 /[ \t]+name="(.*?)"/ =~ cd then @name = $1 end
if /[ \t]+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
@@ -0,0 +1,76 @@
# frozen_string_literal: true
#--
# 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
+156
View File
@@ -0,0 +1,156 @@
# frozen_string_literal: true
#--
# 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
@@ -0,0 +1,381 @@
# frozen_string_literal: true
#
# 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 be 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
+219
View File
@@ -0,0 +1,219 @@
# frozen_string_literal: true
#
# 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 = if $VERBOSE
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
}
else
OpenSSL::PKey::RSA.new(bits)
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
+265
View File
@@ -0,0 +1,265 @@
# frozen_string_literal: true
#
# 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
@@ -0,0 +1,18 @@
# frozen_string_literal: true
#--
# 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.9.1"
end
+24
View File
@@ -0,0 +1,24 @@
module WEBrick
module AccessLog
class AccessLogError < StandardError
end
CLF_TIME_FORMAT: String
COMMON_LOG_FORMAT: String
CLF: String
REFERER_LOG_FORMAT: String
AGENT_LOG_FORMAT: String
COMBINED_LOG_FORMAT: String
def self?.setup_params: (Hash[Symbol, untyped] config, HTTPRequest req, HTTPResponse res) -> Hash[String, untyped]
def self?.format: (String format_string, Hash[String, untyped] params) -> String
def self?.escape: (String data) -> String
end
end
+92
View File
@@ -0,0 +1,92 @@
module WEBrick
class CGI
@options: Array[untyped]
class CGIError < StandardError
end
attr_reader config: Hash[Symbol, untyped]
attr_reader logger: BasicLog
def initialize: (*untyped args) -> void
def []: (Symbol key) -> untyped
interface _Env
def []: (String) -> String?
end
def start: (?_Env env, ?IO stdin, ?IO stdout) -> void
def self.setup_header: () -> untyped
def self.status_line: () -> ""
def service: (HTTPRequest req, HTTPResponse res) -> void
class Socket
@config: Hash[Symbol, untyped]
@env: _Env
@header_part: StringIO
@body_part: IO
@out_port: IO
@server_addr: String
@server_name: String?
@server_port: String?
@remote_addr: String?
@remote_host: String?
@remote_port: (String | 0)
include Enumerable[String]
private
def initialize: (Hash[Symbol, untyped] config, _Env env, IO stdin, IO stdout) -> void
def request_line: () -> String
def setup_header: () -> void
def add_header: (String hdrname, String value) -> void
def input: () -> (IO | StringIO)
public
def peeraddr: () -> [nil, (String | 0), String?, String?]
def addr: () -> [nil, String?, String?, String]
def gets: (?String eol, ?Integer? size) -> String?
def read: (?Integer? size) -> String?
def each: () { (String) -> void } -> void
def eof?: () -> bool
def <<: (_ToS data) -> IO
def write: (_ToS data) -> Integer
def cert: () -> OpenSSL::X509::Certificate?
def peer_cert: () -> OpenSSL::X509::Certificate?
def peer_cert_chain: () -> Array[OpenSSL::X509::Certificate]?
def cipher: () -> [String?, String?, String?, String?]?
end
end
end
+18
View File
@@ -0,0 +1,18 @@
#
# 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
class EPROTO < SystemCallError
end
class ECONNRESET < SystemCallError
end
class ECONNABORTED < SystemCallError
end
end
+17
View File
@@ -0,0 +1,17 @@
module WEBrick
module Config
LIBDIR: String
# for GenericServer
General: Hash[Symbol, untyped]
# for HTTPServer, HTTPRequest, HTTPResponse ...
HTTP: Hash[Symbol, untyped]
FileHandler: Hash[Symbol, untyped]
BasicAuth: Hash[Symbol, untyped]
DigestAuth: Hash[Symbol, untyped]
end
end
+37
View File
@@ -0,0 +1,37 @@
module WEBrick
class Cookie
@expires: String?
attr_reader name: String?
attr_accessor value: String?
attr_accessor version: Integer
#
# The cookie domain
attr_accessor domain: String?
attr_accessor path: String?
attr_accessor secure: true?
attr_accessor comment: String?
attr_accessor max_age: Integer?
def initialize: (untyped name, untyped value) -> void
def expires=: ((Time | _ToS)? t) -> untyped
def expires: () -> Time?
def to_s: () -> String
def self.parse: (String? str) -> Array[instance]?
def self.parse_set_cookie: (String str) -> instance
def self.parse_set_cookies: (String str) -> Array[instance]
end
end
+5
View File
@@ -0,0 +1,5 @@
module WEBrick
module HTMLUtils
def self?.escape: (String? string) -> String
end
end
+13
View File
@@ -0,0 +1,13 @@
module WEBrick
module HTTPAuth
interface _Callable
def call: (String user, String pass) -> bool
end
def self?._basic_auth: (HTTPRequest req, HTTPResponse res, String realm, String req_field, String res_field, HTTPStatus::Error err_type, _Callable block) -> void
def self?.basic_auth: (HTTPRequest req, HTTPResponse res, String realm) { (String user, String pass) -> bool } -> void
def self?.proxy_basic_auth: (HTTPRequest req, HTTPResponse res, String realm) { (String user, String pass) -> bool } -> void
end
end
@@ -0,0 +1,55 @@
module WEBrick
module HTTPAuth
module Authenticator
@reload_db: bool?
@request_field: String
@response_field: String
@resp_info_field: String
@auth_exception: singleton(HTTPStatus::ClientError)
@auth_scheme: String
RequestField: String
ResponseField: String
ResponseInfoField: String
AuthException: singleton(HTTPStatus::ClientError)
AuthScheme: String?
attr_reader realm: String?
attr_reader userdb: UserDB
attr_reader logger: Log
private
def check_init: (Hash[Symbol, untyped] config) -> void
def check_scheme: (HTTPRequest req) -> String?
def log: (interned meth, String fmt, *untyped args) -> void
def error: (String fmt, *untyped args) -> void
def info: (String fmt, *untyped args) -> void
end
module ProxyAuthenticator
RequestField: String
ResponseField: String
ResponseInfoField: String
AuthException: singleton(HTTPStatus::ClientError)
end
end
end
@@ -0,0 +1,29 @@
module WEBrick
module HTTPAuth
class BasicAuth
@config: Hash[Symbol, untyped]
include Authenticator
AuthScheme: String
def self.make_passwd: (String? realm, String? user, String? pass) -> String
attr_reader realm: String?
attr_reader userdb: UserDB
attr_reader logger: Log
def initialize: (Hash[Symbol, untyped] config, ?Hash[Symbol, untyped] default) -> void
def authenticate: (HTTPRequest req, HTTPResponse res) -> void
def challenge: (HTTPRequest req, HTTPResponse res) -> bot
end
class ProxyBasicAuth < BasicAuth
include ProxyAuthenticator
end
end
end
@@ -0,0 +1,85 @@
module WEBrick
module HTTPAuth
class DigestAuth
@config: Hash[Symbol, untyped]
@domain: Array[String]?
@use_opaque: bool
@use_next_nonce: bool
@check_nc: bool
@use_auth_info_header: bool
@nonce_expire_period: Integer
@nonce_expire_delta: Integer
@internet_explorer_hack: bool
@h: singleton(Digest::Base)
@instance_key: String
@opaques: Hash[String, OpaqueInfo]
@last_nonce_expire: Time
@mutex: Thread::Mutex
include Authenticator
AuthScheme: String
class OpaqueInfo < Struct[untyped]
attr_accessor time(): Time
attr_accessor nonce(): String?
attr_accessor nc(): String
end
attr_reader algorithm: String?
attr_reader qop: Array[String]
def self.make_passwd: (String realm, String user, String pass) -> untyped
def initialize: (Hash[Symbol, untyped] config, ?Hash[Symbol, untyped] default) -> void
def authenticate: (HTTPRequest req, HTTPResponse res) -> void
def challenge: (HTTPRequest req, HTTPResponse res, ?bool stale) -> bot
private
MustParams: Array[String]
MustParamsAuth: Array[String]
def _authenticate: (HTTPRequest req, HTTPResponse res) -> (:nonce_is_stale | bool)
def split_param_value: (String string) -> Hash[String, String]
def generate_next_nonce: (HTTPRequest req) -> String
def check_nonce: (HTTPRequest req, Hash[String, String] auth_req) -> bool
def generate_opaque: (HTTPRequest req) -> String
def check_opaque: (OpaqueInfo opaque_struct, untyped req, Hash[String, String] auth_req) -> bool
def check_uri: (HTTPRequest req, Hash[String, String] auth_req) -> bool
def hexdigest: (*_ToS? args) -> String
end
class ProxyDigestAuth < DigestAuth
include ProxyAuthenticator
private
def check_uri: (HTTPRequest req, Hash[String, String] auth_req) -> true
end
end
end
@@ -0,0 +1,31 @@
module WEBrick
module HTTPAuth
class Htdigest
@path: String
@mtime: Time
@digest: Hash[String, Hash[String, String]]
@mutex: Thread::Mutex
@auth_type: String
include UserDB
def initialize: (String path) -> void
def reload: () -> void
def flush: (?String? output) -> void
def get_passwd: (String realm, String user, bool reload_db) -> String?
def set_passwd: (String realm, String user, String pass) -> String
def delete_passwd: (String realm, String user) -> String?
def each: () { (String user, String realm, String password_hash) -> void } -> void
end
end
end
@@ -0,0 +1,21 @@
module WEBrick
module HTTPAuth
class Htgroup
@path: String
@mtime: Time
@group: Hash[String, Array[String]]
def initialize: (String path) -> void
def reload: () -> void
def flush: (?String? output) -> void
def members: (String group) -> Array[String]
def add: (String group, Array[String] members) -> void
end
end
end
@@ -0,0 +1,31 @@
module WEBrick
module HTTPAuth
class Htpasswd
@path: String
@mtime: Time
@passwd: Hash[String, String]
@auth_type: String
@password_hash: (:crypt | :bcrypt)
include UserDB
def initialize: (String path, ?password_hash: (:crypt | :bcrypt)?) -> void
def reload: () -> void
def flush: (?String? output) -> void
def get_passwd: (String realm, String user, bool reload_db) -> String?
def set_passwd: (String realm, String user, String pass) -> void
def delete_passwd: (String realm, String user) -> String
def each: () { ([String, String]) -> void } -> void
end
end
end
@@ -0,0 +1,13 @@
module WEBrick
module HTTPAuth
module UserDB
attr_accessor auth_type: String
def make_passwd: (String realm, String user, String pass) -> String
def set_passwd: (String realm, String user, String pass) -> void
def get_passwd: (String realm, String user, ?bool reload_db) -> String
end
end
end
+61
View File
@@ -0,0 +1,61 @@
module WEBrick
NullReader: untyped
def self.read: (*untyped args) -> nil
alias self.gets self.read
FakeProxyURI: untyped
def self.method_missing: (untyped meth, *untyped args) -> (nil | untyped)
class HTTPProxyServer < HTTPServer
@via: untyped
def initialize: (?::Hash[untyped, untyped] config, ?untyped default) -> void
# :stopdoc:
def service: (HTTPRequest req, HTTPResponse res) -> untyped
def proxy_auth: (HTTPRequest req, HTTPResponse res) -> untyped
def proxy_uri: (HTTPRequest req, HTTPResponse res) -> untyped
def proxy_service: (HTTPRequest req, HTTPResponse res) -> untyped
def do_CONNECT: (HTTPRequest req, HTTPResponse res) -> untyped
def do_GET: (HTTPRequest req, HTTPResponse res) -> untyped
def do_HEAD: (HTTPRequest req, HTTPResponse res) -> untyped
def do_POST: (HTTPRequest req, HTTPResponse res) -> untyped
def do_OPTIONS: (HTTPRequest req, HTTPResponse res) -> untyped
private
# Some header fields should not be transferred.
HopByHop: ::Array["connection" | "keep-alive" | "proxy-authenticate" | "upgrade" | "proxy-authorization" | "te" | "trailers" | "transfer-encoding"]
ShouldNotTransfer: ::Array["set-cookie" | "proxy-connection"]
def split_field: (untyped f) -> (untyped | ::Array[untyped])
def choose_header: (untyped src, untyped dst) -> untyped
# Net::HTTP is stupid about the multiple header fields.
# Here is workaround:
def set_cookie: (untyped src, untyped dst) -> (untyped | nil)
def set_via: (untyped h) -> (untyped | nil)
def setup_proxy_header: (HTTPRequest req, HTTPResponse res) -> untyped
def setup_upstream_proxy_authentication: (HTTPRequest req, HTTPResponse res, untyped header) -> untyped
def create_net_http: (untyped uri, untyped upstream) -> untyped
def perform_proxy_request: (HTTPRequest req, HTTPResponse res, untyped req_class, ?untyped? body_stream) -> untyped
end
end
+167
View File
@@ -0,0 +1,167 @@
module WEBrick
class HTTPRequest
@config: Hash[Symbol, untyped]
@buffer_size: Integer
@logger: Log
@query: Hash[String, HTTPUtils::FormData]?
@body: String
@remaining_size: Integer?
@socket: TCPSocket?
@forwarded_proto: String?
@host: String?
@port: Integer?
@body_tmp: Array[String]
@body_rd: Fiber
@request_bytes: Integer
@forwarded_server: String?
@forwarded_host: String?
@forwarded_port: Integer?
@forwarded_for: String?
BODY_CONTAINABLE_METHODS: Array[String]
attr_reader request_line: String?
attr_reader request_method: String?
attr_reader unparsed_uri: String?
attr_reader http_version: HTTPVersion?
attr_reader request_uri: URI::Generic?
attr_reader path: String?
attr_accessor script_name: String?
attr_accessor path_info: String?
attr_accessor query_string: String?
attr_reader raw_header: Array[String]
attr_reader header: Hash[String, Array[String]]?
attr_reader cookies: Array[Cookie]
attr_reader accept: Array[String]
attr_reader accept_charset: Array[String]
attr_reader accept_encoding: Array[String]
attr_reader accept_language: Array[String]
attr_accessor user: String?
attr_reader addr: ([String, Integer, String, String] | [])
attr_reader peeraddr: ([String, Integer, String, String] | [])
attr_reader attributes: Hash[untyped, untyped]
attr_reader keep_alive: bool
attr_reader request_time: Time?
def initialize: (Hash[Symbol, untyped] config) -> void
def parse: (?TCPSocket? socket) -> void
def continue: () -> void
type body_chunk_block = ^(String body_chunk) -> void
def body: () ?{ (String body_chunk) -> void } -> String
def body_reader: () -> self
# for IO.copy_stream.
def readpartial: (Integer size, ?String buf) -> String
def query: () -> Hash[String, HTTPUtils::FormData]
def content_length: () -> Integer
def content_type: () -> String?
def []: (String header_name) -> String?
def each: [T] () { (String, String) -> T } -> T?
def host: () -> String?
def port: () -> Integer?
def server_name: () -> String?
def remote_ip: () -> String?
def ssl?: () -> bool
def keep_alive?: () -> bool
def to_s: () -> String
def fixup: () -> void
def meta_vars: () -> Hash[String, String]
private
MAX_URI_LENGTH: Integer
# same as Mongrel, Thin and Puma
MAX_HEADER_LENGTH: Integer
def read_request_line: (IO socket) -> void
def read_header: (IO socket) -> void
def parse_uri: (String str, ?String scheme) -> URI::Generic
HOST_PATTERN: Regexp
def parse_host_request_line: (String host) -> [String, String]
def read_body: (IO socket, body_chunk_block block) -> String
| (nil socket, top block) -> nil
def read_chunk_size: (IO socket) -> [Integer, String?]
def read_chunked: (IO socket, body_chunk_block block) -> void
def _read_data: (IO io, Symbol method, *untyped arg) -> String?
def read_line: (IO io, ?Integer size) -> String?
def read_data: (IO io, Integer size) -> String?
def parse_query: () -> void
PrivateNetworkRegexp: Regexp
# 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: () -> void
end
end
+117
View File
@@ -0,0 +1,117 @@
module WEBrick
class HTTPResponse
@buffer_size: Integer
@logger: Log
@chunked: bool
@bodytempfile: File | Tempfile | nil
class InvalidHeader < StandardError
end
attr_reader http_version: HTTPVersion
attr_reader status: Integer
attr_reader header: Hash[String, String]
attr_reader cookies: Array[Cookie]
attr_accessor reason_phrase: String
interface _CallableBody
def call: (_Writer) -> void
end
attr_accessor body: String | _ReaderPartial | _CallableBody
attr_accessor request_method: String?
attr_accessor request_uri: URI::Generic?
attr_accessor request_http_version: HTTPVersion?
attr_accessor filename: String?
attr_accessor keep_alive: bool
attr_reader config: Hash[Symbol, untyped]
attr_reader sent_size: Integer
attr_accessor upgrade: String?
def initialize: (Hash[Symbol, untyped] config) -> void
def status_line: () -> String
def status=: (Integer status) -> Integer
def []: (String field) -> String?
def []=: (String field, _ToS value) -> _ToS
def content_length: () -> Integer?
def content_length=: (Integer len) -> Integer
def content_type: () -> String?
def content_type=: (String type) -> String
def each: () { (String, String) -> void } -> void
def chunked?: () -> bool
def chunked=: (boolish val) -> boolish
def keep_alive?: () -> bool
def upgrade!: (String protocol) -> void
def send_response: (_Writer socket) -> void
def setup_header: () -> void
def make_body_tempfile: () -> void
def remove_body_tempfile: () -> void
def send_header: (_Writer socket) -> void
def send_body: (_Writer socket) -> void
def set_redirect: (singleton(WEBrick::HTTPStatus::Redirect) status, URI::Generic | String url) -> bot
def set_error: (singleton(Exception) ex, ?bool backtrace) -> void
private
def check_header: (_ToS header_value) -> String
def error_body: (bool backtrace, singleton(Exception) ex, String? host, Integer? port) -> void
def send_body_io: (_Writer socket) -> void
def send_body_string: (_Writer socket) -> void
def send_body_proc: (_Writer socket) -> void
class ChunkedWrapper
@socket: _Writer
@resp: HTTPResponse
def initialize: (_Writer socket, HTTPResponse resp) -> void
def write: (_ToS buf) -> Integer
def <<: (*_ToS buf) -> self
end
# preserved for compatibility with some 3rd-party handlers
def _write_data: [T] (T socket, _ToS data) -> T
end
end
+49
View File
@@ -0,0 +1,49 @@
module WEBrick
class HTTPRequest
@client_cert_chain: Array[OpenSSL::X509::Certificate]
attr_reader cipher: [String, String, Integer, Integer]?
attr_reader server_cert: OpenSSL::X509::Certificate
attr_reader client_cert: OpenSSL::X509::Certificate?
alias orig_parse parse
def parse: (?(TCPSocket | OpenSSL::SSL::SSLSocket)? socket) -> void
| ...
alias orig_parse_uri parse_uri
private
def parse_uri: (String str, ?::String scheme) -> URI::Generic
| ...
public
alias orig_meta_vars meta_vars
def meta_vars: () -> Hash[String, String]
| ...
end
class SNIRequest
attr_reader host: String?
attr_reader addr: [String, Integer, String, String]
attr_reader port: Integer
def initialize: (OpenSSL::SSL::SSLSocket sslsocket, ?String? hostname) -> void
end
class HTTPServer < ::WEBrick::GenericServer
def ssl_servername_callback: (OpenSSL::SSL::SSLSocket sslsocket, ?String? hostname) -> OpenSSL::SSL::SSLContext?
alias orig_virtual_host virtual_host
def virtual_host: (instance server) -> void
| ...
end
end
+71
View File
@@ -0,0 +1,71 @@
module WEBrick
class HTTPServerError < ServerError
end
class HTTPServer < ::WEBrick::GenericServer
@http_version: HTTPVersion
@mount_tab: MountTable
@virtual_hosts: Array[untyped]
def initialize: (?Hash[Symbol, untyped] config, ?Hash[Symbol, untyped] default) -> void
def run: (TCPSocket sock) -> void
def service: (HTTPRequest req, HTTPResponse res) -> void
def do_OPTIONS: (HTTPRequest req, HTTPResponse res) -> void
def mount: (String dir, singleton(HTTPServlet::AbstractServlet) servlet, *untyped options) -> void
def mount_proc: (String dir, ?HTTPServlet::ProcHandler::_Callable proc) -> void
| (String dir, ?nil proc) { (HTTPRequest, HTTPResponse) -> void } -> void
def unmount: (String dir) -> MountTable::value_type
alias umount unmount
def search_servlet: (String path) -> [singleton(HTTPServlet::AbstractServlet), Array[untyped], String, String]?
def virtual_host: (instance server) -> void
def lookup_server: (HTTPRequest req) -> instance?
def access_log: (Hash[Symbol, untyped] config, HTTPRequest req, HTTPResponse res) -> void
#
# Creates the HTTPRequest used when handling the HTTP
# request. Can be overridden by subclasses.
def create_request: (Hash[Symbol, untyped] with_webrick_config) -> HTTPRequest
#
# Creates the HTTPResponse used when handling the HTTP
# request. Can be overridden by subclasses.
def create_response: (Hash[Symbol, untyped] with_webrick_config) -> HTTPResponse
class MountTable
type value_type = [singleton(HTTPServlet::AbstractServlet), Array[untyped]]
@tab: Hash[String, value_type]
@scanner: Regexp
def initialize: () -> void
def []: (String dir) -> value_type
def []=: (String dir, value_type val) -> value_type
def delete: (String dir) -> value_type
def scan: (String path) -> [String, String]
private
def compile: () -> void
def normalize: (String dir) -> String
end
end
end
@@ -0,0 +1,4 @@
module WEBrick
module HTTPServlet
end
end
@@ -0,0 +1,36 @@
module WEBrick
module HTTPServlet
class HTTPServletError < StandardError
end
class AbstractServlet
@server: HTTPServer
interface _Config
def []: (Symbol) -> untyped
end
@config: _Config
@logger: Log
@options: untyped # Array[untyped] causes RBS::InstanceVariableTypeError
def self.get_instance: (HTTPServer server, *untyped options) -> instance
def initialize: (HTTPServer server, *untyped options) -> void
def service: (HTTPRequest req, HTTPResponse res) -> void
def do_GET: (HTTPRequest req, HTTPResponse res) -> bot
def do_HEAD: (HTTPRequest req, HTTPResponse res) -> bot
def do_OPTIONS: (HTTPRequest req, HTTPResponse res) -> void
private
def redirect_to_directory_uri: (HTTPRequest req, HTTPResponse res) -> void
end
end
end
@@ -0,0 +1,3 @@
class Object
def sysread: (IO io, Integer size) -> String
end
@@ -0,0 +1,23 @@
module WEBrick
module HTTPServlet
class CGIHandler < AbstractServlet
@script_filename: String
@tempdir: String?
@cgicmd: Array[String] | String
Ruby: String
CGIRunner: String
CGIRunnerArray: [String, String]
def initialize: (HTTPServer server, String name) -> void
def do_GET: (HTTPRequest req, HTTPResponse res) -> void
alias do_POST do_GET
end
end
end
@@ -0,0 +1,17 @@
module WEBrick
module HTTPServlet
class ERBHandler < AbstractServlet
@script_filename: String
def initialize: (HTTPServer server, String name) -> void
def do_GET: (HTTPRequest req, HTTPResponse res) -> void
alias do_POST do_GET
private
def evaluate: (ERB erb, HTTPRequest servlet_request, HTTPResponse servlet_response) -> String
end
end
end
@@ -0,0 +1,76 @@
module WEBrick
module HTTPServlet
class DefaultFileHandler < AbstractServlet
@local_path: String
def initialize: (HTTPServer server, String local_path) -> void
def do_GET: (HTTPRequest req, HTTPResponse res) -> void
def not_modified?: (HTTPRequest req, HTTPResponse res, Time mtime, String etag) -> bool
# returns a lambda for webrick/httpresponse.rb send_body_proc
def multipart_body: (File body, Array[[Numeric, Numeric]] parts, String boundary, String mtype, Integer filesize) -> HTTPResponse::_CallableBody
def make_partial_content: (HTTPRequest req, HTTPResponse res, String filename, Integer filesize) -> void
def prepare_range: (Range[Integer] range, Integer filesize) -> [Numeric, Numeric]
end
class FileHandler < AbstractServlet
@config: AbstractServlet::_Config
@logger: Log
@root: String
@options: Hash[Symbol, untyped]
HandlerTable: Hash[String, singleton(AbstractServlet)]
def self.add_handler: (String suffix, singleton(AbstractServlet) handler) -> singleton(AbstractServlet)
def self.remove_handler: (String suffix) -> singleton(AbstractServlet)
def initialize: (HTTPServer server, String root, ?Hash[Symbol, untyped] options, ?Hash[Symbol, untyped] default) -> void
def set_filesystem_encoding: (String str) -> String
def service: (HTTPRequest req, HTTPResponse res) -> void
def do_GET: (HTTPRequest req, HTTPResponse res) -> void
def do_POST: (HTTPRequest req, HTTPResponse res) -> void
def do_OPTIONS: (HTTPRequest req, HTTPResponse res) -> void
private
def trailing_pathsep?: (String path) -> bool
def prevent_directory_traversal: (HTTPRequest req, HTTPResponse res) -> void
def exec_handler: (HTTPRequest req, HTTPResponse res) -> bool
def get_handler: (HTTPRequest req, HTTPResponse res) -> singleton(AbstractServlet)
def set_filename: (HTTPRequest req, HTTPResponse res) -> bool
def check_filename: (HTTPRequest req, HTTPResponse res, String name) -> void
def shift_path_info: (HTTPRequest req, HTTPResponse res, String path_info, ?String? base) -> void
def search_index_file: (HTTPRequest req, HTTPResponse res) -> String?
def search_file: (HTTPRequest req, HTTPResponse res, String basename) -> String?
def call_callback: (Symbol callback_name, HTTPRequest req, HTTPResponse res) -> void
def windows_ambiguous_name?: (String name) -> bool
def nondisclosure_name?: (String name) -> bool
def set_dir_list: (HTTPRequest req, HTTPResponse res) -> void
end
end
end
@@ -0,0 +1,21 @@
module WEBrick
module HTTPServlet
class ProcHandler < AbstractServlet
interface _Callable
def call: (WEBrick::HTTPRequest, WEBrick::HTTPResponse) -> void
end
@proc: _Callable
def get_instance: (HTTPServer server, *untyped options) -> self
def initialize: (_Callable proc) -> void
def do_GET: (HTTPRequest request, HTTPResponse response) -> void
alias do_POST do_GET
alias do_PUT do_GET
end
end
end
+255
View File
@@ -0,0 +1,255 @@
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
attr_reader self.code: Integer
attr_reader self.reason_phrase: String
# Returns the HTTP status code
def code: () -> Integer
# Returns the HTTP status description
def reason_phrase: () -> String
alias to_i code
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: Hash[Integer, String]
# Maps a status code to the corresponding Status class
CodeToError: Hash[Integer, singleton(Status)]
# WEBrick::HTTPStatus::constants.grep(/\ARC_/).map{"#{_1}: #{WEBrick::HTTPStatus.const_get(_1)}"}
RC_CONTINUE: 100
RC_SWITCHING_PROTOCOLS: 101
RC_OK: 200
RC_CREATED: 201
RC_ACCEPTED: 202
RC_NON_AUTHORITATIVE_INFORMATION: 203
RC_NO_CONTENT: 204
RC_RESET_CONTENT: 205
RC_PARTIAL_CONTENT: 206
RC_MULTI_STATUS: 207
RC_MULTIPLE_CHOICES: 300
RC_MOVED_PERMANENTLY: 301
RC_FOUND: 302
RC_SEE_OTHER: 303
RC_NOT_MODIFIED: 304
RC_USE_PROXY: 305
RC_TEMPORARY_REDIRECT: 307
RC_BAD_REQUEST: 400
RC_UNAUTHORIZED: 401
RC_PAYMENT_REQUIRED: 402
RC_FORBIDDEN: 403
RC_NOT_FOUND: 404
RC_METHOD_NOT_ALLOWED: 405
RC_NOT_ACCEPTABLE: 406
RC_PROXY_AUTHENTICATION_REQUIRED: 407
RC_REQUEST_TIMEOUT: 408
RC_CONFLICT: 409
RC_GONE: 410
RC_PRECONDITION_FAILED: 412
RC_LENGTH_REQUIRED: 411
RC_REQUEST_ENTITY_TOO_LARGE: 413
RC_REQUEST_URI_TOO_LARGE: 414
RC_UNSUPPORTED_MEDIA_TYPE: 415
RC_EXPECTATION_FAILED: 417
RC_UNPROCESSABLE_ENTITY: 422
RC_LOCKED: 423
RC_FAILED_DEPENDENCY: 424
RC_REQUEST_RANGE_NOT_SATISFIABLE: 416
RC_UPGRADE_REQUIRED: 426
RC_PRECONDITION_REQUIRED: 428
RC_TOO_MANY_REQUESTS: 429
RC_REQUEST_HEADER_FIELDS_TOO_LARGE: 431
RC_UNAVAILABLE_FOR_LEGAL_REASONS: 451
RC_INTERNAL_SERVER_ERROR: 500
RC_NOT_IMPLEMENTED: 501
RC_BAD_GATEWAY: 502
RC_SERVICE_UNAVAILABLE: 503
RC_GATEWAY_TIMEOUT: 504
RC_HTTP_VERSION_NOT_SUPPORTED: 505
RC_INSUFFICIENT_STORAGE: 507
RC_NETWORK_AUTHENTICATION_REQUIRED: 511
# WEBrick::HTTPStatus::CodeToError.each_value.map{"class #{_1.name.split(/::/).last} < #{_1.superclass.name.split(/::/).last}\nend"}
class Continue < Info
end
class SwitchingProtocols < Info
end
class OK < Success
end
class Created < Success
end
class Accepted < Success
end
class NonAuthoritativeInformation < Success
end
class NoContent < Success
end
class ResetContent < Success
end
class PartialContent < Success
end
class MultiStatus < Success
end
class MultipleChoices < Redirect
end
class MovedPermanently < Redirect
end
class Found < Redirect
end
class SeeOther < Redirect
end
class NotModified < Redirect
end
class UseProxy < Redirect
end
class TemporaryRedirect < Redirect
end
class BadRequest < ClientError
end
class Unauthorized < ClientError
end
class PaymentRequired < ClientError
end
class Forbidden < ClientError
end
class NotFound < ClientError
end
class MethodNotAllowed < ClientError
end
class NotAcceptable < ClientError
end
class ProxyAuthenticationRequired < ClientError
end
class RequestTimeout < ClientError
end
class Conflict < ClientError
end
class Gone < ClientError
end
class LengthRequired < ClientError
end
class PreconditionFailed < ClientError
end
class RequestEntityTooLarge < ClientError
end
class RequestURITooLarge < ClientError
end
class UnsupportedMediaType < ClientError
end
class RequestRangeNotSatisfiable < ClientError
end
class ExpectationFailed < ClientError
end
class UnprocessableEntity < ClientError
end
class Locked < ClientError
end
class FailedDependency < ClientError
end
class UpgradeRequired < ClientError
end
class PreconditionRequired < ClientError
end
class TooManyRequests < ClientError
end
class RequestHeaderFieldsTooLarge < ClientError
end
class UnavailableForLegalReasons < ClientError
end
class InternalServerError < ServerError
end
class NotImplemented < ServerError
end
class BadGateway < ServerError
end
class ServiceUnavailable < ServerError
end
class GatewayTimeout < ServerError
end
class HTTPVersionNotSupported < ServerError
end
class InsufficientStorage < ServerError
end
class NetworkAuthenticationRequired < ServerError
end
#
# Returns the description corresponding to the HTTP status +code+
#
# WEBrick::HTTPStatus.reason_phrase 404
# => "Not Found"
def self?.reason_phrase: (Integer code) -> String
#
# Is +code+ an informational status?
def self?.info?: (Integer code) -> bool
#
# Is +code+ a successful status?
def self?.success?: (Integer code) -> bool
#
# Is +code+ a redirection status?
def self?.redirect?: (Integer code) -> bool
#
# Is +code+ an error status?
def self?.error?: (Integer code) -> bool
#
# Is +code+ a client error status?
def self?.client_error?: (Integer code) -> bool
#
# Is +code+ a server error status?
def self?.server_error?: (Integer code) -> bool
#
# Returns the status class corresponding to +code+
#
# WEBrick::HTTPStatus[302]
# => WEBrick::HTTPStatus::NotFound
#
def self.[]: (Integer code) -> singleton(Status)
end
end
+116
View File
@@ -0,0 +1,116 @@
module WEBrick
CR: String
LF: String
CRLF: String
module HTTPUtils
def self?.normalize_path: (String path) -> String
type mime_types = Hash[String, String]
DefaultMimeTypes: mime_types
def self?.load_mime_types: (string | _ToPath file) -> mime_types
def self?.mime_type: (String filename, mime_types mime_tab) -> String
class SplitHeader < Array[String]
def join: (?String separator) -> String
end
class CookieHeader < Array[String]
def join: (?String separator) -> String
end
HEADER_CLASSES: Hash[String, untyped]
def self?.parse_header: (String raw) -> Hash[String, Array[String]]
def self?.split_header_value: (String str) -> Array[String]
def self?.parse_range_header: (String? ranges_specifier) -> Array[Range[Integer]]?
def self?.parse_qvalues: (String? value) -> Array[String]
def self?.dequote: (String str) -> String
def self?.quote: (String str) -> String
class FormData < String
@raw_header: Array[String]
@header: Hash[String, Array[String]]
EmptyRawHeader: Array[String]
EmptyHeader: Hash[String, Array[String]]
attr_accessor name: String?
attr_accessor filename: String?
attr_accessor next_data: instance?
def initialize: (*String args) -> void
def []: (*String key) -> String
# following is as same as String#[]
| (int start, ?int length) -> String?
| (range[int?] range) -> String?
| (Regexp regexp, ?MatchData::capture backref) -> String?
| (String substring) -> String?
def <<: (String str) -> self
def append_data: (instance data) -> self
def each_data: () { (instance) -> void } -> void
def list: () -> Array[String]
alias to_ary list
def to_s: () -> String
end
def self?.parse_query: (String? str) -> Hash[String, FormData]
interface _EachLine
def each_line: () { (String) -> void } -> void
end
def self?.parse_form_data: (_EachLine? io, interned boundary) -> Hash[String, FormData]
def self?._make_regex: (String str) -> Regexp
def self?._make_regex!: (String str) -> Regexp
def self?._escape: (String str, Regexp regex) -> String
def self?._unescape: (String str, Regexp regex) -> String
UNESCAPED: Regexp
UNESCAPED_FORM: Regexp
NONASCII: Regexp
ESCAPED: Regexp
UNESCAPED_PCHAR: Regexp
def self?.escape: (String str) -> String
def self?.unescape: (String str) -> String
def self?.escape_form: (String str) -> String
def self?.unescape_form: (String str) -> String
def self?.escape_path: (String str) -> String
def self?.escape8bit: (String str) -> String
end
end
@@ -0,0 +1,17 @@
module WEBrick
class HTTPVersion
include Comparable
attr_accessor major: Integer
attr_accessor minor: Integer
def self.convert: (HTTPVersion | String version) -> instance
def initialize: (HTTPVersion | String version) -> void
def <=>: (HTTPVersion | String other) -> Integer?
def to_s: () -> String
end
end
+93
View File
@@ -0,0 +1,93 @@
module WEBrick
class BasicLog
@log: IO?
@opened: TrueClass?
FATAL: 1
ERROR: 2
WARN: 3
INFO: 4
DEBUG: 5
# log-level, messages above this level will be logged
attr_accessor level: Integer
type log_file = (IO | String)?
def initialize: (?log_file log_file, ?Integer? level) -> void
#
# Closes the logger (also closes the log device associated to the logger)
def close: () -> void
def log: (Integer level, String data) -> IO?
#
# Synonym for log(INFO, obj.to_s)
def <<: (_ToS obj) -> IO?
type message = Exception | _ToStr | Object
# Shortcut for logging a FATAL message
def fatal: (message msg) -> IO?
# Shortcut for logging an ERROR message
def error: (message msg) -> IO?
# Shortcut for logging a WARN message
def warn: (message msg) -> IO?
# Shortcut for logging an INFO message
def info: (message msg) -> IO?
# Shortcut for logging a DEBUG message
def debug: (message msg) -> IO?
# Will the logger output FATAL messages?
def fatal?: () -> bool
# Will the logger output ERROR messages?
def error?: () -> bool
# Will the logger output WARN messages?
def warn?: () -> bool
# Will the logger output INFO messages?
def info?: () -> bool
# Will the logger output DEBUG messages?
def debug?: () -> bool
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: (message arg) -> String
end
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: String
#
# Same as BasicLog#initialize
#
# You can set the timestamp format through #time_format
def initialize: (?BasicLog::log_file log_file, ?Integer? level) -> void
#
# Same as BasicLog#log
def log: (Integer level, String data) -> IO?
end
end
+8
View File
@@ -0,0 +1,8 @@
dependencies:
- name: digest
- name: erb
- name: openssl
- name: singleton
- name: socket
- name: tempfile
- name: uri
+57
View File
@@ -0,0 +1,57 @@
module WEBrick
class ServerError < StandardError
end
class SimpleServer
def self.start: [T] () { () -> T } -> T
end
class Daemon
def self.start: () -> void
| [T] () { () -> T } -> T
end
class GenericServer
@shutdown_pipe: [IO, IO]?
attr_reader status: :Stop | :Running | :Shutdown
attr_reader config: Hash[Symbol, untyped]
attr_reader logger: BasicLog
attr_reader tokens: Thread::SizedQueue
attr_reader listeners: Array[TCPServer| OpenSSL::SSL::SSLServer]
def initialize: (?Hash[Symbol, untyped] config, ?Hash[Symbol, untyped] default) -> void
def []: (Symbol key) -> untyped
def listen: (String address, Integer port) -> void
def start: () ?{ (TCPSocket) -> void } -> void
def stop: () -> void
def shutdown: () -> void
def run: (TCPSocket sock) -> void
private
def accept_client: (TCPServer svr) -> TCPSocket?
def start_thread: (TCPSocket sock) ?{ (TCPSocket) -> void } -> Thread
def call_callback: (Symbol callback_name, *untyped args) -> untyped
def setup_shutdown_pipe: () -> [IO, IO]
def cleanup_shutdown_pipe: ([IO, IO]? shutdown_pipe) -> void
def alarm_shutdown_pipe: [T] () { (IO) -> T } -> T?
def cleanup_listener: () -> void
end
end
+19
View File
@@ -0,0 +1,19 @@
module WEBrick
module Config
SSL: Hash[Symbol, untyped]
end
module Utils
def self?.create_self_signed_cert: (untyped bits, untyped cn, untyped comment) -> ::Array[untyped]
end
class GenericServer
@ssl_context: OpenSSL::SSL::SSLContext?
def ssl_context: () -> OpenSSL::SSL::SSLContext?
def setup_ssl_context: (Hash[Symbol, untyped] config) -> OpenSSL::SSL::SSLContext
def ssl_servername_callback: (untyped sslsocket, ?untyped? hostname) -> untyped
end
end
+122
View File
@@ -0,0 +1,122 @@
module WEBrick
module Utils
#
# Sets IO operations on +io+ to be non-blocking
def self?.set_non_blocking: (IO io) -> void
#
# Sets the close on exec flag for +io+
def self?.set_close_on_exec: (IO io) -> void
#
# Changes the process's uid and gid to the ones of +user+
def self?.su: (String user) -> void
#
# The server hostname
def self?.getservername: () -> String
#
# Creates TCP server sockets bound to +address+:+port+ and returns them.
#
# It will create IPV4 and IPV6 sockets on all interfaces.
def self?.create_listeners: (String host, Integer port) -> Array[TCPServer]
#
# Characters used to generate random strings
RAND_CHARS: String
#
# Generates a random string of length +len+
def self?.random_string: (Integer len) -> String
#
# 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
@queue: Thread::Queue
@watcher: Thread?
include Singleton
#
# Mutex used to synchronize access across threads
TimeoutMutex: Thread::Mutex
#
# Registers a new timeout handler
#
# +time+:: Timeout in seconds
# +exception+:: Exception to raise when timeout elapsed
def self.register: (Numeric seconds, singleton(Exception) exception) -> Integer
#
# Cancels the timeout handler +id+
def self.cancel: (Integer id) -> bool
def self.terminate: () -> Thread?
#
# Creates a new TimeoutHandler. You should use ::register and ::cancel
# instead of creating the timeout handler directly.
def initialize: () -> void
private
def watch: () -> bot
def watcher: () -> Thread
public
#
# Interrupts the timeout handler +id+ and raises +exception+
def interrupt: (Thread thread, Integer id, singleton(Exception) exception) -> nil
#
# Registers a new timeout handler
#
# +time+:: Timeout in seconds
# +exception+:: Exception to raise when timeout elapsed
def register: (Thread thread, Numeric time, singleton(Exception) exception) -> Integer
#
# Cancels the timeout handler +id+
def cancel: (Thread thread, Integer id) -> bool
#
def terminate: () -> Thread?
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 self?.timeout: [T] (Numeric? seconds, ?singleton(Exception) exception) { (?Numeric) -> T } -> T
end
end
+3
View File
@@ -0,0 +1,3 @@
module WEBrick
VERSION: String
end
+105
View File
@@ -0,0 +1,105 @@
# frozen_string_literal: true
begin
require_relative 'lib/webrick/version'
rescue LoadError
# for Ruby core repository
require_relative 'version'
end
Gem::Specification.new do |s|
s.name = "webrick"
s.version = WEBrick::VERSION
s.summary = "HTTP server toolkit"
s.description = "WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server."
s.require_path = %w{lib}
s.files = [
"Gemfile",
"LICENSE.txt",
"README.md",
"Rakefile",
"lib/webrick.rb",
"lib/webrick/accesslog.rb",
"lib/webrick/cgi.rb",
"lib/webrick/compat.rb",
"lib/webrick/config.rb",
"lib/webrick/cookie.rb",
"lib/webrick/htmlutils.rb",
"lib/webrick/httpauth.rb",
"lib/webrick/httpauth/authenticator.rb",
"lib/webrick/httpauth/basicauth.rb",
"lib/webrick/httpauth/digestauth.rb",
"lib/webrick/httpauth/htdigest.rb",
"lib/webrick/httpauth/htgroup.rb",
"lib/webrick/httpauth/htpasswd.rb",
"lib/webrick/httpauth/userdb.rb",
"lib/webrick/httpproxy.rb",
"lib/webrick/httprequest.rb",
"lib/webrick/httpresponse.rb",
"lib/webrick/https.rb",
"lib/webrick/httpserver.rb",
"lib/webrick/httpservlet.rb",
"lib/webrick/httpservlet/abstract.rb",
"lib/webrick/httpservlet/cgi_runner.rb",
"lib/webrick/httpservlet/cgihandler.rb",
"lib/webrick/httpservlet/erbhandler.rb",
"lib/webrick/httpservlet/filehandler.rb",
"lib/webrick/httpservlet/prochandler.rb",
"lib/webrick/httpstatus.rb",
"lib/webrick/httputils.rb",
"lib/webrick/httpversion.rb",
"lib/webrick/log.rb",
"lib/webrick/server.rb",
"lib/webrick/ssl.rb",
"lib/webrick/utils.rb",
"lib/webrick/version.rb",
"sig/accesslog.rbs",
"sig/cgi.rbs",
"sig/compat.rbs",
"sig/config.rbs",
"sig/cookie.rbs",
"sig/htmlutils.rbs",
"sig/httpauth.rbs",
"sig/httpauth/authenticator.rbs",
"sig/httpauth/basicauth.rbs",
"sig/httpauth/digestauth.rbs",
"sig/httpauth/htdigest.rbs",
"sig/httpauth/htgroup.rbs",
"sig/httpauth/htpasswd.rbs",
"sig/httpauth/userdb.rbs",
"sig/httpproxy.rbs",
"sig/httprequest.rbs",
"sig/httpresponse.rbs",
"sig/https.rbs",
"sig/httpserver.rbs",
"sig/httpservlet.rbs",
"sig/httpservlet/abstract.rbs",
"sig/httpservlet/cgi_runner.rbs",
"sig/httpservlet/cgihandler.rbs",
"sig/httpservlet/erbhandler.rbs",
"sig/httpservlet/filehandler.rbs",
"sig/httpservlet/prochandler.rbs",
"sig/httpstatus.rbs",
"sig/httputils.rbs",
"sig/httpversion.rbs",
"sig/log.rbs",
"sig/manifest.yaml",
"sig/server.rbs",
"sig/ssl.rbs",
"sig/utils.rbs",
"sig/version.rbs",
"webrick.gemspec",
]
s.required_ruby_version = ">= 2.4.0"
s.authors = ["TAKAHASHI Masayoshi", "GOTOU YUUZOU", "Eric Wong"]
s.email = [nil, nil, 'normal@ruby-lang.org']
s.homepage = "https://github.com/ruby/webrick"
s.licenses = ["Ruby", "BSD-2-Clause"]
if s.respond_to?(:metadata=)
s.metadata = {
"bug_tracker_uri" => "https://github.com/ruby/webrick/issues",
}
end
end