1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

* lib/webrick: imported.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@4130 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
gotoyuzo 2003-07-23 16:51:36 +00:00
parent 02f036ddbc
commit 01eba908ad
33 changed files with 3881 additions and 0 deletions

29
lib/webrick.rb Normal file
View file

@ -0,0 +1,29 @@
#
# WEBrick -- WEB server toolkit.
#
# 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 $
require 'webrick/compat.rb'
require 'webrick/version.rb'
require 'webrick/config.rb'
require 'webrick/log.rb'
require 'webrick/server.rb'
require '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'

64
lib/webrick/accesslog.rb Normal file
View file

@ -0,0 +1,64 @@
#
# 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
module AccessLog
class AccessLogError < StandardError; end
CLF_TIME_FORMAT = "[%d/%b/%Y:%H:%M:%S %Z]"
COMMON_LOG_FORMAT = "%h %l %u %t \"%r\" %s %b"
CLF = COMMON_LOG_FORMAT
REFERER_LOG_FORMAT = "%{Referer}i -> %U"
AGENT_LOG_FORMAT = "%{User-Agent}i"
COMBINED_LOG_FORMAT = "#{CLF} \"%{Referer}i\" \"%{User-agent}i\""
module_function
# This format specification is a subset of mod_log_config of Apache.
# http://httpd.apache.org/docs/mod/mod_log_config.html#formats
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["o"] = res
params["p"] = config[: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
def format(format_string, params)
format_string.gsub(/\%(?:\{(.*?)\})?>?([a-zA-Z])/){
param, spec = $1, $2
case spec[0]
when ?e, ?i, ?o
raise AccessLogError,
"parameter is required for \"#{spec}\"" unless param
params[spec][param] || "-"
when ?t
params[spec].strftime(param || CLF_TIME_FORMAT)
else
params[spec]
end
}
end
end
end

30
lib/webrick/compat.rb Normal file
View file

@ -0,0 +1,30 @@
#
# 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 $
module Errno
class EPROTO < SystemCallError; end
class ECONNRESET < SystemCallError; end
class ECONNABORTED < SystemCallError; end
end
unless File.respond_to?(:fnmatch)
def File.fnmatch(pat, str)
case pat[0]
when nil
not str[0]
when ?*
fnmatch(pat[1..-1], str) || str[0] && fnmatch(pat, str[1..-1])
when ??
str[0] && fnmatch(pat[1..-1], str[1..-1])
else
pat[0] == str[0] && fnmatch(pat[1..-1], str[1..-1])
end
end
end

96
lib/webrick/config.rb Normal file
View file

@ -0,0 +1,96 @@
#
# 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 'webrick/version'
require 'webrick/httpversion'
require 'webrick/httputils'
require 'webrick/utils'
require 'webrick/log'
module WEBrick
module Config
LIBDIR = File::dirname(__FILE__)
# for GenericServer
General = {
:ServerName => Utils::getservername,
:BindAddress => nil, # "0.0.0.0" or "::" or nil
:Port => nil, # users MUST specifiy this!!
:Listen => [], # list of pairs of alt addr/port.
: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,
}
# 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 },
:RequestHandler => nil,
:ProxyAuthProc => nil,
:ProxyContentHandler => nil,
:ProxyVia => true,
:ProxyTimeout => true,
# upstream proxy server
:ProxyURI => nil,
:CGIInterpreter => nil,
:CGIPathEnv => nil,
# workaround: if Request-URIs contain 8bit chars,
# they should be escaped before calling of URI::parse().
:Escape8bitURI => false
)
FileHandler = {
:NondisclosureName => ".ht*",
:FancyIndexing => false,
:HandlerTable => {},
:HandlerCallback => nil,
:DirectoryCallback => nil,
:FileCallback => nil,
:UserDir => "public_html",
}
BasicAuth = {
:AutoReloadUserDB => true,
}
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

80
lib/webrick/cookie.rb Normal file
View file

@ -0,0 +1,80 @@
#
# 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 'webrick/httputils'
module WEBrick
class Cookie
attr_reader :name
attr_accessor :value, :version
attr_accessor :domain, :path, :secure
attr_accessor :comment, :max_age
#attr_accessor :comment_url, :discard, :port
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
def expires=(t)
@expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s)
end
def expires
@expires && Time.parse(@expires)
end
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
# Cookie::parse()
# It parses Cookie field sent from the user agent.
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
end
end

25
lib/webrick/htmlutils.rb Normal file
View file

@ -0,0 +1,25 @@
#
# 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
def escape(string)
str = string ? string.dup : ""
str.gsub!(/&/n, '&amp;')
str.gsub!(/\"/n, '&quot;')
str.gsub!(/>/n, '&gt;')
str.gsub!(/</n, '&lt;')
str
end
module_function :escape
end
end

46
lib/webrick/httpauth.rb Normal file
View file

@ -0,0 +1,46 @@
#
# 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 'base64'
require 'webrick/httpauth/basicauth'
require 'webrick/httpauth/digestauth'
require 'webrick/httpauth/htpasswd'
require 'webrick/httpauth/htdigest'
require 'webrick/httpauth/htgroup'
module WEBrick
module HTTPAuth
module_function
def _basic_auth(req, res, realm, req_field, res_field, err_type, block)
user = pass = nil
if /^Basic\s+(.*)/o =~ req[req_field]
userpass = $1
user, pass = decode64(userpass).split(":", 2)
end
if block.call(user, pass)
req.user = user
return
end
res[res_field] = "Basic realm=\"#{realm}\""
raise err_type
end
def basic_auth(req, res, realm, &block)
_basic_auth(req, res, realm, "Authorization", "WWW-Authenticate",
HTTPStatus::Unauthorized, block)
end
def proxy_basic_auth(req, res, realm, &block)
_basic_auth(req, res, realm, "Proxy-Authorization", "Proxy-Authenticate",
HTTPStatus::ProxyAuthenticationRequired, block)
end
end
end

View file

@ -0,0 +1,79 @@
#
# 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 Authenticator
RequestField = "Authorization"
ResponseField = "WWW-Authenticate"
ResponseInfoField = "Authentication-Info"
AuthException = HTTPStatus::Unauthorized
AuthScheme = nil # must override by the derived class
attr_reader :realm, :userdb, :logger
private
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
def check_scheme(req)
unless credentials = req[@request_field]
error("no credentials in the request.")
return nil
end
unless match = /^#{@auth_scheme}\s+/.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
end
module ProxyAuthenticator
RequestField = "Proxy-Authorization"
ResponseField = "Proxy-Authenticate"
InfoField = "Proxy-Authentication-Info"
AuthException = HTTPStatus::ProxyAuthenticationRequired
end
end
end

View file

@ -0,0 +1,66 @@
#
# 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 'webrick/config'
require 'webrick/httpstatus'
require 'webrick/httpauth/authenticator'
require 'base64'
module WEBrick
module HTTPAuth
class BasicAuth
include Authenticator
AuthScheme = "Basic"
def self.make_passwd(realm, user, pass)
pass ||= ""
pass.crypt(Utils::random_string(2))
end
attr_reader :realm, :userdb, :logger
def initialize(config, default=Config::BasicAuth)
check_init(config)
@config = default.dup.update(config)
end
def authenticate(req, res)
unless basic_credentials = check_scheme(req)
challenge(req, res)
end
userid, password = decode64(basic_credentials).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
if password.crypt(encpass) != encpass
error("%s: password unmatch.", userid)
challenge(req, res)
end
info("%s: authentication succeeded.", userid)
req.user = userid
end
def challenge(req, res)
res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\""
raise @auth_exception
end
end
class ProxyBasicAuth < BasicAuth
include ProxyAuthenticator
end
end
end

View file

@ -0,0 +1,348 @@
#
# 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 'webrick/config'
require 'webrick/httpstatus'
require 'webrick/httpauth/authenticator'
require 'digest/md5'
require 'digest/sha1'
require 'base64'
module WEBrick
module HTTPAuth
class DigestAuth
include Authenticator
AuthScheme = "Digest"
OpaqueInfo = Struct.new(:time, :nonce, :nc)
attr_reader :algorithm, :qop
def self.make_passwd(realm, user, pass)
pass ||= ""
Digest::MD5::hexdigest([user, realm, pass].join(":"))
end
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]
@opera_hack = @config[:OperaHack]
case @algorithm
when 'MD5','MD5-sess'
@h = Digest::MD5
when 'SHA1','SHA1-sess' # it is a bonus feature :-)
@h = Digest::SHA1
else
msg = format('Alogrithm "%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 = Mutex.new
end
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
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
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'] != @algorithm &&
(@opera_hack && auth_req['algorithm'] != @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 allowd.', 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$/ =~ auth_req['algorithm'] ||
(@opera_hack && /-SESS$/ =~ 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 = digest("", auth_req['uri'])
elsif auth_req['qop'] == "auth-int"
ha2 = hexdigest(req.request_method, auth_req['uri'],
hexdigest(req.body))
ha2_res = digest("", auth_req['uri'], hexdigest(req.body))
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 scceeded.', auth_req['username'])
req.user = auth_req['username']
return true
end
def split_param_value(string)
ret = {}
while string.size != 0
case string
when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/
key = $1
matched = $2
string = $'
ret[key] = matched.gsub(/\\(.)/, "\\1")
when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/
key = $1
matched = $2
string = $'
ret[key] = matched.clone
when /^s*^,/
string = $'
else
break
end
end
ret
end
def generate_next_nonce(req)
now = "%012d" % req.request_time.to_i
pk = hexdigest(now, @instance_key)[0,32]
nonce = encode64(now + ":" + pk).chop # 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 = decode64(nonce).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
def digest(*args)
@h.digest(args.join(":"))
end
end
class ProxyDigestAuth < DigestAuth
include ProxyAuthenticator
def check_uri(req, auth_req)
return true
end
end
end
end

View file

@ -0,0 +1,91 @@
#
# 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 'webrick/httpauth/userdb'
require 'webrick/httpauth/digestauth'
require 'tempfile'
module WEBrick
module HTTPAuth
class Htdigest
include UserDB
def initialize(path)
@path = path
@mtime = Time.at(0)
@digest = Hash.new
@mutex = Mutex::new
@auth_type = DigestAuth
open(@path,"a").close unless File::exist?(@path)
reload
end
def reload
mtime = File::mtime(@path)
if mtime > @mtime
@digest.clear
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
def flush(output=nil)
output ||= @path
tmp = Tempfile.new("htpasswd", File::dirname(output))
begin
each{|item| tmp.puts(item.join(":")) }
tmp.close
File::rename(tmp.path, output)
rescue
tmp.close(true)
end
end
def get_passwd(realm, user, reload_db)
reload() if reload_db
if hash = @digest[realm]
hash[user]
end
end
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
def delete_passwd(realm, user)
if hash = @digest[realm]
hash.delete(user)
end
end
def each
@digest.keys.sort.each{|realm|
hash = @digest[realm]
hash.keys.sort.each{|user|
yield([user, realm, hash[user]])
}
}
end
end
end
end

View file

@ -0,0 +1,61 @@
#
# 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
class Htgroup
def initialize(path)
@path = path
@mtime = Time.at(0)
@group = Hash.new
open(@path,"a").close unless File::exist?(@path)
reload
end
def reload
if (mtime = File::mtime(@path)) > @mtime
@group.clear
open(@path){|io|
while line = io.gets
line.chomp!
group, members = line.split(/:\s*/)
@group[group] = members.split(/\s+/)
end
}
@mtime = mtime
end
end
def flush(output=nil)
output ||= @path
tmp = Tempfile.new("htgroup", File::dirname(output))
begin
@group.keys.sort.each{|group|
tmp.puts(format("%s: %s", group, self.members(group).join(" ")))
}
tmp.close
File::rename(tmp.path, output)
rescue
tmp.close(true)
end
end
def members(group)
reload
@group[group] || []
end
def add(group, members)
@group[group] = members(group) | members
end
end
end
end

View file

@ -0,0 +1,75 @@
#
# 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 'webrick/httpauth/userdb'
require 'webrick/httpauth/basicauth'
require 'tempfile'
module WEBrick
module HTTPAuth
class Htpasswd
include UserDB
def initialize(path)
@path = path
@mtime = Time.at(0)
@passwd = Hash.new
@auth_type = BasicAuth
open(@path,"a").close unless File::exist?(@path)
reload
end
def reload
mtime = File::mtime(@path)
if mtime > @mtime
@passwd.clear
open(@path){|io|
while line = io.gets
line.chomp!
user, pass = line.split(":")
@passwd[user] = pass
end
}
@mtime = mtime
end
end
def flush(output=nil)
output ||= @path
tmp = Tempfile.new("htpasswd", File::dirname(output))
begin
each{|item| tmp.puts(item.join(":")) }
tmp.close
File::rename(tmp.path, output)
rescue
tmp.close(true)
end
end
def get_passwd(realm, user, reload_db)
reload() if reload_db
@passwd[user]
end
def set_passwd(realm, user, pass)
@passwd[user] = make_passwd(realm, user, pass)
end
def delete_passwd(realm, user)
@passwd.delete(user)
end
def each
@passwd.keys.sort.each{|user|
yield([user, @passwd[user]])
}
end
end
end
end

View file

@ -0,0 +1,29 @@
#
# 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
module UserDB
attr_accessor :auth_type # BasicAuth or DigestAuth
def make_passwd(realm, user, pass)
@auth_type::make_passwd(realm, user, pass)
end
def set_passwd(realm, user, pass)
self[user] = pass
end
def get_passwd(realm, user, reload_db=false)
# reload_db is dummy
make_passwd(realm, user, self[user])
end
end
end
end

237
lib/webrick/httpproxy.rb Normal file
View file

@ -0,0 +1,237 @@
#
# 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 "webrick/httpserver"
require "net/http"
Net::HTTP::version_1_2 if RUBY_VERSION < "1.7"
module WEBrick
class HTTPProxyServer < HTTPServer
def initialize(config)
super
c = @config
@via = "#{c[:HTTPVersion]} #{c[:ServerName]}:#{c[:Port]}"
end
def service(req, res)
if req.request_method == "CONNECT"
proxy_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
# Some header fields shuold not be transfered.
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 proxy_uri(req, res)
@config[:ProxyURI]
end
def proxy_service(req, res)
# Proxy Authentication
proxy_auth(req, res)
# Create Request-URI to send to the origin server
uri = req.request_uri
path = uri.path.dup
path << "?" << uri.query if uri.query
# Choose header fields to transfer
header = Hash.new
choose_header(req, header)
set_via(header)
# select upstream proxy server
if proxy = proxy_uri(req, res)
proxy_host = proxy.host
proxy_port = proxy.port
if proxy.userinfo
credentials = "Basic " + encode64(proxy.userinfo)
header['proxy-authorization'] = credentials
end
end
response = nil
begin
http = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port)
http.start{
if @config[:ProxyTimeout]
################################## these issues are
http.open_timeout = 30 # secs # necessary (maybe bacause
http.read_timeout = 60 # secs # Ruby's bug, but why?)
##################################
end
case req.request_method
when "GET" then response = http.get(path, header)
when "POST" then response = http.post(path, req.body || "", header)
when "HEAD" then response = http.head(path, header)
else
raise HTTPStatus::MethodNotAllowed,
"unsupported method `#{req.request_method}'."
end
}
rescue => err
logger.debug("#{err.class}: #{err.message}")
raise HTTPStatus::ServiceUnavailable, err.message
end
# Persistent connction requirements are mysterious for me.
# So I will close the connection in every response.
res['proxy-connection'] = "close"
res['connection'] = "close"
# Convert Net::HTTP::HTTPResponse to WEBrick::HTTPProxy
res.status = response.code.to_i
choose_header(response, res)
set_cookie(response, res)
set_via(res)
res.body = response.body
# Process contents
if handler = @config[:ProxyContentHandler]
handler.call(req, res)
end
end
def proxy_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 " + encode64(proxy.userinfo)
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 a credentials")
os << "Proxy-Authorization: " << credentials << CRLF
end
os << CRLF
proxy_status_line = os.gets(LF)
@logger.debug("CONNECT: read a Status-Line form 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
res.send_response(ua)
access_log(@config, req, res)
end
begin
while fds = IO::select([ua, os])
if fds[0].member?(ua)
buf = ua.sysread(1024);
@logger.debug("CONNECT: #{buf.size} byte from User-Agent")
os.syswrite(buf)
elsif fds[0].member?(os)
buf = os.sysread(1024);
@logger.debug("CONNECT: #{buf.size} byte from #{host}:#{port}")
ua.syswrite(buf)
end
end
rescue => ex
os.close
@logger.debug("CONNECT #{host}:#{port}: closed")
end
raise HTTPStatus::EOFError
end
def do_OPTIONS(req, res)
res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT"
end
end
end

339
lib/webrick/httprequest.rb Normal file
View file

@ -0,0 +1,339 @@
#
# 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 'timeout'
require 'uri'
require 'webrick/httpversion'
require 'webrick/httpstatus'
require 'webrick/httputils'
require 'webrick/cookie'
module WEBrick
class HTTPRequest
BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ]
BUFSIZE = 1024*4
# Request line
attr_reader :request_line
attr_reader :request_method, :unparsed_uri, :http_version
# Request-URI
attr_reader :request_uri, :host, :port, :path, :query_string
attr_accessor :script_name, :path_info
# Header and entity body
attr_reader :raw_header, :header, :cookies
# Misc
attr_accessor :user
attr_reader :addr, :peeraddr
attr_reader :attributes
attr_reader :keep_alive
attr_reader :request_time
def initialize(config)
@config = config
@logger = config[:Logger]
@request_line = @request_method =
@unparsed_uri = @http_version = nil
@request_uri = @host = @port = @path = nil
@script_name = @path_info = nil
@query_string = nil
@query = nil
@form_data = nil
@raw_header = Array.new
@header = nil
@cookies = []
@body = ""
@addr = @peeraddr = nil
@attributes = {}
@user = nil
@keep_alive = false
@request_time = nil
@remaining_size = nil
@socket = nil
end
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)
}
end
return if @request_method == "CONNECT"
return if @unparsed_uri == "*"
begin
@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 /close/io =~ self["connection"]
@keep_alive = false
elsif /keep-alive/io =~ self["connection"]
@keep_alive = true
elsif @http_version < "1.1"
@keep_alive = false
else
@keep_alive = true
end
end
def body(&block)
block ||= Proc.new{|chunk| @body << chunk }
read_body(@socket, block)
@body.empty? ? nil : @body
end
def query
unless @query
parse_query()
end
@query
end
def [](header_name)
if @header
value = @header[header_name.downcase]
value.empty? ? nil : value.join(", ")
end
end
def each
@header.each{|k, v|
value = @header[k]
yield(k, value.empty? ? nil : value.join(", "))
}
end
def keep_alive?
@keep_alive
end
def to_s
ret = @request_line.dup
@raw_header.each{|line| ret << line }
ret << CRLF
ret << body if body
ret
end
def fixup()
begin
body{|chunk| } # read remaining body
rescue HTTPStatus::Error => ex
@logger.error("HTTPRequest#fixup: #{ex.class} occured.")
@keep_alive = false
rescue => ex
@logger.error(ex)
@keep_alive = false
end
end
def meta_vars
# This method provides the metavariables defined by the revision 3
# of ``The WWW Common Gateway Interface Version 1.1''.
# (http://Web.Golux.Com/coar/cgi/)
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.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"] = @request_uri.host
meta["SERVER_PORT"] = @config[:Port].to_s
meta["SERVER_PROTOCOL"] = "HTTP/" + @config[:HTTPVersion].to_s
meta["SERVER_SOFTWARE"] = @config[:ServerSoftware].dup
self.each{|key, val|
name = "HTTP_" + key
name.gsub!(/-/o, "_")
name.upcase!
meta[name] = val
}
meta
end
private
def read_request_line(socket)
@request_line = read_line(socket) if socket
@request_time = Time.now
raise HTTPStatus::EOFError unless @request_line
if /^(\S+)\s+(\S+)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
@request_method = $1
@unparsed_uri = $2
@http_version = HTTPVersion.new($3 ? $3 : "0.9")
else
rl = @request_line.sub(/\x0d?\x0a\z/o, '')
raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
end
end
def read_header(socket)
if socket
while line = read_line(socket)
break if /\A(#{CRLF}|#{LF})\z/om =~ line
@raw_header << line
end
end
begin
@header = HTTPUtils::parse_header(@raw_header)
rescue => ex
raise HTTPStatus::BadRequest, ex.message
end
end
def parse_uri(str, scheme="http")
if @config[:Escape8bitURI]
str = HTTPUtils::escape8bit(str)
end
uri = URI::parse(str)
return uri if uri.absolute?
if self["host"]
host, port = self['host'].split(":", 2)
elsif @addr.size > 0
host, port = @addr[2], @addr[1]
else
host, port = @config[:ServerName], @config[:Port]
end
uri.scheme = scheme
uri.host = host
uri.port = port ? port.to_i : nil
return URI::parse(uri.to_s)
end
def read_body(socket, block)
return unless socket
if tc = self['transfer-encoding']
case tc
when /chunked/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 = BUFSIZE < @remaining_size ? BUFSIZE : @remaining_size
break unless buf = read_data(socket, sz)
@remaining_size -= buf.size
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 /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line
chunk_size = $1.hex
chunk_ext = $2
[ chunk_size, chunk_ext ]
else
raise HTTPStatus::BadRequest, "bad chunk `#{line}'."
end
end
def read_chunked(socket, block)
chunk_size, = read_chunk_size(socket)
while chunk_size > 0
data = read_data(socket, chunk_size) # read chunk-data
if data.nil? || data.size != chunk_size
raise BadRequest, "bad chunk data size."
end
read_line(socket) # skip CRLF
block.call(data)
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
timeout(@config[:RequestTimeout]){
return io.__send__(method, arg)
}
rescue Errno::ECONNRESET
return nil
rescue TimeoutError
raise HTTPStatus::RequestTimeout
end
end
def read_line(io)
_read_data(io, :gets, LF)
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
end
end

304
lib/webrick/httpresponse.rb Normal file
View file

@ -0,0 +1,304 @@
#
# 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 'webrick/httpversion'
require 'webrick/htmlutils'
require 'webrick/httputils'
require 'webrick/httpstatus'
module WEBrick
class HTTPResponse
BUFSIZE = 1024*4
attr_reader :http_version, :status, :header
attr_reader :cookies
attr_accessor :reason_phrase
attr_accessor :body
attr_accessor :request_method, :request_uri, :request_http_version
attr_accessor :filename
attr_reader :config, :keep_alive, :sent_size
def initialize(config)
@config = config
@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
end
def status_line
"HTTP/#@http_version #@status #@reason_phrase #{CRLF}"
end
def status=(status)
@status = status
@reason_phrase = HTTPStatus::reason_phrase(status)
end
def [](field)
@header[field.downcase]
end
def []=(field, value)
@header[field.downcase] = value.to_s
end
def each
@header.each{|k, v| yield(k, v) }
end
def chunked?
@chunked
end
def chunked=(val)
@chunked = val ? true : false
end
def keep_alive?
@keep_alive
end
def send_response(socket)
begin
setup_header()
send_header(socket)
send_body(socket)
rescue Errno::EPIPE
@logger.error("HTTPResponse#send_response: EPIPE occured.")
@keep_alive = false
rescue => ex
@logger.error(ex)
@keep_alive = false
end
end
def setup_header()
@reason_phrase ||= HTTPStatus::reason_phrase(@status)
@header['server'] ||= @config[:ServerSoftware]
@header['date'] ||= Time.now.httpdate
# HTTP/0.9 features
if @request_http_version < "1.0"
@http_version = HTTPVersion.new("0.9")
@keep_alive = false
end
# HTTP/1.0 features
if @request_http_version < "1.1"
if chunked?
@chunked = false
ver = @request_http_version.to_s
msg = "chunked is set for an HTTP/#{ver} request. (ignored)"
@logger.warn(msg)
end
end
# Determin 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?
unless @body.is_a?(IO)
@header['content-length'] = @body ? @body.size : 0
end
end
# Keep-Alive connection.
if @header['connection'] == "close"
@keep_alive = false
end
if keep_alive?
if chunked? || @header['content-length']
@header['connection'] = "Keep-Alive"
end
end
# Location is a single absoluteURI.
if location = @header['location']
if @request_uri
@header['location'] = @request_uri.merge(location)
end
end
end
def send_header(socket)
if @http_version.major > 0
data = status_line()
@header.each{|key, value|
tmp = key.gsub(/\bwww|^te$|\b\w/){|s| s.upcase }
data << "#{tmp}: #{value}" << CRLF
}
@cookies.each{|cookie|
data << "Set-Cookie: " << cookie.to_s << CRLF
}
data << CRLF
_write_data(socket, data)
end
end
def send_body(socket)
case @body
when IO then send_body_io(socket)
else send_body_string(socket)
end
end
def to_s
ret = ""
send_response(ret)
ret
end
def set_redirect(status, url)
@body = "<HTML><A HREF=\"#{url.to_s}\">#{url.to_s}</A>.</HTML>\n"
@header['location'] = url.to_s
raise status
end
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"
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
@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
private
def send_body_io(socket)
if @request_method == "HEAD"
# do nothing
elsif chunked?
while buf = @body.read(BUFSIZE)
next if buf.empty?
data = ""
data << format("%x", buf.size) << CRLF
data << buf << CRLF
_write_data(socket, data)
@sent_size += buf.size
end
_write_data(socket, "0#{CRLF}#{CRLF}")
else
size = @header['content-length'].to_i
_send_file(socket, @body, 0, size.to_i)
@sent_size = size
end
@body.close
end
def send_body_string(socket)
if @request_method == "HEAD"
# do nothing
elsif chunked?
remain = body ? @body.size : 0
while buf = @body[@sent_size, BUFSIZE]
break if buf.empty?
data = ""
data << format("%x", buf.size) << CRLF
data << buf << CRLF
_write_data(socket, data)
@sent_size += buf.size
end
_write_data(socket, "0#{CRLF}#{CRLF}")
else
if @body && @body.size > 0
_write_data(socket, @body)
@sent_size = @body.size
end
end
end
def _send_file(output, input, offset, size)
while offset > 0
sz = BUFSIZE < offset ? BUFSIZE : offset
buf = input.read(sz)
offset -= buf.size
end
if size == 0
while buf = input.read(BUFSIZE)
_write_data(output, buf)
end
else
while size > 0
sz = BUFSIZE < size ? BUFSIZE : size
buf = input.read(sz)
_write_data(output, buf)
size -= buf.size
end
end
end
def _write_data(socket, data)
socket << data
end
end
end

158
lib/webrick/https.rb Normal file
View file

@ -0,0 +1,158 @@
#
# 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 'webrick'
require 'openssl'
module WEBrick
module Config
HTTP.update(
:SSLEnable => true,
:SSLCertificate => nil,
:SSLPrivateKey => nil,
:SSLClientCA => nil,
:SSLCACertificateFile => nil,
:SSLCACertificatePath => nil,
:SSLCertStore => nil,
:SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
:SSLVerifyDepth => nil,
:SSLVerifyCallback => nil, # custom verification
:SSLTimeout => nil,
:SSLOptions => nil,
# Must specify if you use auto generated certificate.
:SSLCertName => nil,
:SSLCertComment => "Generated by Ruby/OpenSSL"
)
osslv = ::OpenSSL::OPENSSL_VERSION.split[1]
HTTP[:ServerSoftware] << " OpenSSL/#{osslv}"
end
class HTTPRequest
attr_reader :cipher, :server_cert, :client_cert
alias orig_parse parse
def parse(socket=nil)
orig_parse(socket)
@cipher = socket.respond_to?(:cipher) ? socket.cipher : nil
@client_cert = socket.respond_to?(:peer_cert) ? socket.peer_cert : nil
@server_cert = @config[:SSLCertificate]
end
alias orig_parse_uri parse_uri
def parse_uri(str, scheme="https")
if @config[:SSLEnable]
return orig_parse_uri(str, scheme)
end
return orig_parse_uri(str)
end
alias orig_meta_vars meta_vars
def meta_vars
meta = orig_meta_vars
if @config[:SSLEnable]
meta["HTTPS"] = "on"
meta["SSL_CIPHER"] = @cipher ? @cipher[0] : ""
meta["SSL_CLIENT_CERT"] = @client_cert ? @client_cert.to_pem : ""
meta["SSL_SERVER_CERT"] = @server_cert ? @server_cert.to_pem : ""
end
meta
end
end
class HTTPServer
alias orig_init initialize
def initialize(*args)
orig_init(*args)
if @config[:SSLEnable]
unless @config[:SSLCertificate]
rsa = OpenSSL::PKey::RSA.new(512){|p, n|
case p
when 0; $stderr.putc "." # BN_generate_prime
when 1; $stderr.putc "+" # BN_generate_prime
when 2; $stderr.putc "*" # searching good prime,
# n = #of try,
# but also data from BN_generate_prime
when 3; $stderr.putc "\n" # found good prime, n==0 - p, n==1 - q,
# but also data from BN_generate_prime
else; $stderr.putc "*" # BN_generate_prime
end
}
cert = OpenSSL::X509::Certificate.new
cert.version = 3
cert.serial = 0
name = OpenSSL::X509::Name.new(@config[:SSLCertName])
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)
cert.extensions = [
ef.create_extension("basicConstraints","CA:FALSE"),
ef.create_extension("subjectKeyIdentifier", "hash"),
ef.create_extension("extendedKeyUsage", "serverAuth")
]
ef.issuer_certificate = cert
ext = ef.create_extension("authorityKeyIdentifier",
"keyid:always,issuer:always")
cert.add_extension(ext)
if comment = @config[:SSLCertComment]
cert.add_extension(ef.create_extension("nsComment", comment))
end
cert.sign(rsa, OpenSSL::Digest::SHA1.new)
@config[:SSLPrivateKey] = rsa
@config[:SSLCertificate] = cert
@logger.info cert.to_s
end
@ctx = OpenSSL::SSL::SSLContext.new
set_ssl_context(@ctx, @config)
end
end
alias orig_run run
def run(sock)
if @config[:SSLEnable]
ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx)
ssl.accept
Thread.current[:WEBrickSocket] = ssl
orig_run(ssl)
Thread.current[:WEBrickSocket] = sock
ssl.close
else
orig_run(sock)
end
end
private
def set_ssl_context(ctx, config)
ctx.key = config[:SSLPrivateKey]
ctx.cert = config[:SSLCertificate]
ctx.client_ca = config[:SSLClientCA]
ctx.ca_file = config[:SSLCACertificateFile]
ctx.ca_path = config[:SSLCACertificatePath]
ctx.cert_store = config[:SSLCertStore]
ctx.verify_mode = config[:SSLVerifyClient]
ctx.verify_depth = config[:SSLVerifyDepth]
ctx.verify_callback = config[:SSLVerifyCallback]
ctx.timeout = config[:SSLTimeout]
ctx.options = config[:SSLOptions]
end
end
end

179
lib/webrick/httpserver.rb Normal file
View file

@ -0,0 +1,179 @@
#
# 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 'webrick/server'
require 'webrick/httputils'
require 'webrick/httpstatus'
require 'webrick/httprequest'
require 'webrick/httpresponse'
require 'webrick/httpservlet'
require 'webrick/accesslog'
module WEBrick
class HTTPServerError < ServerError; end
class HTTPServer < ::WEBrick::GenericServer
def initialize(config={}, default=Config::HTTP)
super
@http_version = HTTPVersion::convert(@config[:HTTPVersion])
@mount_tab = MountTable.new
if @config[:DocumentRoot]
mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot],
@config[:DocumentRootOptions])
end
unless @config[:AccessLog]
basic_log = BasicLog::new
@config[:AccessLog] = [
[ basic_log, AccessLog::COMMON_LOG_FORMAT ],
[ basic_log, AccessLog::REFERER_LOG_FORMAT ]
]
end
end
def run(sock)
while true
res = HTTPResponse.new(@config)
req = HTTPRequest.new(@config)
begin
req.parse(sock)
res.request_method = req.request_method
res.request_uri = req.request_uri
res.request_http_version = req.http_version
if handler = @config[:RequestHandler]
handler.call(req, res)
end
service(req, res)
rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex
res.set_error(ex)
rescue HTTPStatus::Error => ex
res.set_error(ex)
rescue HTTPStatus::Status => ex
res.status = ex.code
rescue StandardError, NameError => ex # for Ruby 1.6
@logger.error(ex)
res.set_error(ex, true)
ensure
if req.request_line
req.fixup()
res.send_response(sock)
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
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
def do_OPTIONS(req, res)
res["allow"] = "GET,HEAD,POST,OPTIONS"
end
def mount(dir, servlet, *options)
@logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir))
@mount_tab[dir] = [ servlet, options ]
end
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
def unmount(dir)
@logger.debug(sprintf("unmount %s.", inspect, dir))
@mount_tab.delete(dir)
end
alias umount unmount
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
def access_log(config, req, res)
param = AccessLog::setup_params(config, req, res)
level = Log::INFO
@config[:AccessLog].each{|logger, fmt|
logger.log(level, AccessLog::format(fmt, param))
}
end
class MountTable
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("^(" + k.join("|") +")(?=/|$)")
end
def normalize(dir)
ret = dir ? dir.dup : ""
ret.sub!(%r|/+$|, "")
ret
end
end
end
end

View file

@ -0,0 +1,22 @@
#
# 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 'webrick/httpservlet/abstract'
require 'webrick/httpservlet/filehandler'
require 'webrick/httpservlet/cgihandler'
require 'webrick/httpservlet/erbhandler'
require 'webrick/httpservlet/prochandler'
module WEBrick
module HTTPServlet
FileHandler.add_handler("cgi", CGIHandler)
FileHandler.add_handler("rhtml", ERBHandler)
end
end

View file

@ -0,0 +1,71 @@
#
# 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 'thread'
require 'webrick/htmlutils'
require 'webrick/httputils'
require 'webrick/httpstatus'
module WEBrick
module HTTPServlet
class HTTPServletError < StandardError; end
class AbstractServlet
def self.get_instance(config, *options)
self.new(config, *options)
end
def initialize(server, *options)
@server = @config = server
@logger = @server[:Logger]
@options = options
end
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
def do_GET(req, res)
raise HTTPStatus::NotFound, "not found."
end
def do_HEAD(req, res)
do_GET(req, res)
end
def do_OPTIONS(req, res)
m = self.methods.grep(/^do_[A-Z]+$/)
m.collect!{|i| i.sub(/do_/, "") }
m.sort!
res["allow"] = m.join(",")
end
private
def redirect_to_directory_uri(req, res)
if req.path[-1] != ?/
location = req.path + "/"
if req.query_string && req.query_string.size > 0
location << "?" << req.query_string
end
res.set_redirect(HTTPStatus::MovedPermanently, location)
end
end
end
end
end

View file

@ -0,0 +1,45 @@
#
# 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.size
end
return buf
end
STDIN.binmode
buf = ""
len = sysread(STDIN, 8).to_i
out = sysread(STDIN, len)
STDOUT.reopen(open(out, "w"))
len = sysread(STDIN, 8).to_i
err = sysread(STDIN, len)
STDERR.reopen(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 interpreter = ARGV[0]
exec(interpreter, ENV["SCRIPT_FILENAME"])
# NOTREACHED
end
exec ENV["SCRIPT_FILENAME"]

View file

@ -0,0 +1,93 @@
#
# 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 'webrick/config'
require 'webrick/httpservlet/abstract'
module WEBrick
module HTTPServlet
class CGIHandler < AbstractServlet
Ruby = File::join(::Config::CONFIG['bindir'],
::Config::CONFIG['ruby_install_name'])
CGIRunner = "#{Ruby} #{Config::LIBDIR}/httpservlet/cgi_runner.rb"
def initialize(server, name)
super
@script_filename = name
@tempdir = server[:TempDir]
@cgicmd = "#{CGIRunner} #{server[:CGIInterpreter]}"
end
def do_GET(req, res)
data = nil
status = -1
cgi_in = IO::popen(@cgicmd, "w")
cgi_out = Tempfile.new("webrick.cgiout.", @tempdir)
cgi_err = Tempfile.new("webrick.cgierr.", @tempdir)
begin
cgi_in.sync = true
meta = req.meta_vars
meta["SCRIPT_FILENAME"] = @script_filename
meta["PATH"] = @config[:CGIPathEnv]
dump = Marshal.dump(meta)
cgi_in.write("%8d" % cgi_out.path.size)
cgi_in.write(cgi_out.path)
cgi_in.write("%8d" % cgi_err.path.size)
cgi_in.write(cgi_err.path)
cgi_in.write("%8d" % dump.size)
cgi_in.write(dump)
if req.body and req.body.size > 0
cgi_in.write(req.body)
end
ensure
cgi_in.close
status = $? >> 8
data = cgi_out.read
cgi_out.close(true)
if errmsg = cgi_err.read
if errmsg.size > 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]+/on, 2)
raise HTTPStatus::InternalServerError,
"The server encontered a script error." if body.nil?
begin
header = HTTPUtils::parse_header(raw_header)
if /^(\d+)/ =~ header['status'][0]
res.status = $1.to_i
header.delete('status')
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
end
end
end

View file

@ -0,0 +1,53 @@
#
# 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 'webrick/httpservlet/abstract.rb'
require 'erb'
module WEBrick
module HTTPServlet
class ERBHandler < AbstractServlet
def initialize(server, name)
super
@script_filename = name
end
def do_GET(req, res)
unless defined?(ERB)
@logger.warn "#{self.class}: ERB not defined."
raise HTTPStatus::Forbidden, "ERBHandler cannot work."
end
begin
data = open(@script_filename){|io| io.read }
res.body = evaluate(ERB.new(data), req, res)
res['content-type'] = "text/html"
rescue StandardError => ex
raise
rescue Exception => ex
@logger.error(ex)
raise HTTPStatus::InternalServerError, ex.message
end
end
alias do_POST do_GET
private
def evaluate(erb, servlet_request, servlet_response)
Module.new.module_eval{
meta_vars = servlet_request.meta_vars
query = servlet_request.query
erb.result(binding)
}
end
end
end
end

View file

@ -0,0 +1,330 @@
#
# 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 'thread'
require 'time'
require 'webrick/htmlutils'
require 'webrick/httputils'
require 'webrick/httpstatus'
module WEBrick
module HTTPServlet
class DefaultFileHandler < AbstractServlet
def initialize(server, local_path)
super
@local_path = local_path
end
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
res['last-modified'] = mtime.httpdate
res.body = 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_valie(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
def make_partial_content(req, res, filename, filesize)
mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes])
unless ranges = HTTPUtils::parse_range_header(req['range'])
raise BadRequest, "Unrecognized range-spec: \"#{range}\""
end
open(filename, "rb"){|io|
if ranges.size > 1
boundary = "#{time.sec}_#{time.usec}_#{Process::pid}"
body = ''
ranges.each{|r|
first, last = prepare_range(range, filesize)
next if first < 0
io.pos = first
content = io.read(last-first+1)
body << "--" << boundary << CRLF
body << "Content-Type: #{mtype}" << CRLF
body << "Content-Range: #{first}-#{last}/#{filesize}" << CRLF
body << CRLF
body << content
body << CRLF
}
raise HTTPStatus::RequestRangeNotSatisfiable if body.empty?
body << "--" << boundary << "--" << CRLF
elsif range = ranges[0]
first, last = prepare_range(range, filesize)
raise HTTPStatus::RequestRangeNotSatisfiable if first < 0
if last == filesize - 1
content = io.dup
content.pos = first
else
io.pos = first
content = io.read(last-first+1)
end
res['content-type'] = mtype
res['content-range'] = "#{first}-#{last}/#{filesize}"
res['content-length'] = last - first + 1
res.body = content
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
end
class FileHandler < AbstractServlet
HandlerTable = Hash.new(DefaultFileHandler)
def self.add_handler(suffix, handler)
HandlerTable[suffix] = handler
end
def self.remove_handler(suffix)
HandlerTable.delete(suffix)
end
def initialize(server, root, options={}, default=Config::FileHandler)
@config = server.config
@logger = @config[:Logger]
@root = root
if options == true || options == false
options = { :FancyIndexing => options }
end
@options = default.dup.update(options)
end
def service(req, res)
# if this class is mounted on "/" and /~username is requested.
# we're going to override path informations 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
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 exec_handler(req, res)
raise HTTPStatus::NotFound, "`#{req.path}' not found" unless @root
if set_filename(req, res)
suffix = (/\.(\w+)$/ =~ res.filename) && $1
handler = @options[:HandlerTable][suffix] || HandlerTable[suffix]
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 set_filename(req, res)
handler = nil
res.filename = @root.dup
path_info = req.path_info.scan(%r|/[^/]*|)
while name = path_info.shift
if name == "/"
indices = @config[:DirectoryIndex]
index = indices.find{|i| FileTest::file?("#{res.filename}/#{i}") }
name = "/#{index}" if index
end
res.filename << name
req.script_name << name
req.path_info = path_info.join
if File::fnmatch("/#{@options[:NondisclosureName]}", name)
@logger.log(Log::WARN,
"the request refers nondisclosure name `#{name}'.")
raise HTTPStatus::Forbidden, "`#{req.path}' not found."
end
st = (File::stat(res.filename) rescue nil)
raise HTTPStatus::NotFound, "`#{req.path}' not found." unless st
raise HTTPStatus::Forbidden,
"no access permission to `#{req.path}'." unless st.readable?
if st.directory?
call_callback(:DirectoryCallback, req, res)
else
call_callback(:FileCallback, req, res)
return true
end
end
return false
end
def call_callback(callback_name, req, res)
if cb = @options[callback_name]
cb.call(req, res)
end
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 File::fnmatch(@options[:NondisclosureName], name)
st = (File::stat(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!
if d0 = req.query["N"]; idx = 0
elsif d0 = req.query["M"]; idx = 1
elsif d0 = req.query["S"]; idx = 2
else d0 = "A" ; idx = 0
end
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
res['content-type'] = "text/html"
res.body = <<-_end_of_html_
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
<HEAD><TITLE>Index of #{HTMLUtils::escape(req.path)}</TITLE></HEAD>
<BODY>
<H1>Index of #{HTMLUtils::escape(req.path)}</H1>
_end_of_html_
res.body << "<PRE>\n"
res.body << " <A HREF=\"?N=#{d1}\">Name</A> "
res.body << "<A HREF=\"?M=#{d1}\">Last modified</A> "
res.body << "<A HREF=\"?S=#{d1}\">Size</A>\n"
res.body << "<HR>\n"
list.unshift [ "..", File::mtime(local_path+".."), -1 ]
list.each{ |name, time, size|
if name == ".."
dname = "Parent Directory"
elsif name.size > 25
dname = name.sub(/^(.{23})(.*)/){ $1 + ".." }
else
dname = name
end
s = " <A HREF=\"#{HTTPUtils::escape(name)}\">#{dname}</A>"
s << " " * (30 - dname.size)
s << (time ? time.strftime("%Y/%m/%d %H:%M ") : " " * 22)
s << (size >= 0 ? size.to_s : "-") << "\n"
res.body << s
}
res.body << "</PRE><HR>"
res.body << <<-_end_of_html_
<ADDRESS>
#{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
at #{req.request_uri.host}:#{@config[:Port]}
</ADDRESS>
</BODY>
</HTML>
_end_of_html_
end
end
end
end

View file

@ -0,0 +1,33 @@
#
# 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 'webrick/httpservlet/abstract.rb'
module WEBrick
module HTTPServlet
class ProcHandler < AbstractServlet
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
end
end
end

126
lib/webrick/httpstatus.rb Normal file
View file

@ -0,0 +1,126 @@
#
# 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 $
module WEBrick
module HTTPStatus
class Status < StandardError; end
class Info < Status; end
class Success < Status; end
class Redirect < Status; end
class Error < Status; end
class ClientError < Error; end
class ServerError < Error; end
class EOFError < StandardError; end
StatusMessage = {
100, 'Continue',
101, 'Switching Protocols',
200, 'OK',
201, 'Created',
202, 'Accepted',
203, 'Non-Authoritative Information',
204, 'No Content',
205, 'Reset Content',
206, 'Partial Content',
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',
500, 'Internal Server Error',
501, 'Not Implemented',
502, 'Bad Gateway',
503, 'Service Unavailable',
504, 'Gateway Timeout',
505, 'HTTP Version Not Supported'
}
CodeToError = {}
StatusMessage.each{|code, message|
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
eval %-
RC_#{var_name} = #{code}
class #{err_name} < #{parent}
def self.code() RC_#{var_name} end
def self.reason_phrase() StatusMessage[code] end
def code() self::class::code end
def reason_phrase() self::class::reason_phrase end
alias to_i code
end
-
CodeToError[code] = const_get(err_name)
}
def reason_phrase(code)
StatusMessage[code.to_i]
end
def info?(code)
code.to_i >= 100 and code.to_i < 200
end
def success?(code)
code.to_i >= 200 and code.to_i < 300
end
def redirect?(code)
code.to_i >= 300 and code.to_i < 400
end
def error?(code)
code.to_i >= 400 and code.to_i < 600
end
def client_error?(code)
code.to_i >= 400 and code.to_i < 500
end
def server_error?(code)
code.to_i >= 500 and code.to_i < 600
end
def self.[](code)
CodeToError[code]
end
module_function :reason_phrase
module_function :info?, :success?, :redirect?, :error?
module_function :client_error?, :server_error?
end
end

374
lib/webrick/httputils.rb Normal file
View file

@ -0,0 +1,374 @@
#
# 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"
LF = "\x0a"
CRLF = "\x0d\x0a"
module HTTPUtils
def normalize_path(path)
raise "abnormal path `#{path}'" if path[0] != ?/
ret = path.dup
ret.gsub!(%r{/+}o, '/') # // => /
while ret.sub!(%r{/\.(/|\Z)}o, '/'); end # /. => /
begin # /foo/.. => /foo
match = ret.sub!(%r{/([^/]+)/\.\.(/|\Z)}o){
if $1 == ".."
raise "abnormal path `#{path}'"
else
"/"
end
}
end while match
raise "abnormal path `#{path}'" if %r{/\.\.(/|\Z)} =~ ret
ret
end
module_function :normalize_path
#####
DefaultMimeTypes = {
"ai" => "application/postscript",
"asc" => "text/plain",
"avi" => "video/x-msvideo",
"bin" => "application/octet-stream",
"bmp" => "image/bmp",
"class" => "application/octet-stream",
"cer" => "application/pkix-cert",
"crl" => "application/pkix-crl",
"crt" => "application/x-x509-ca-cert",
#"crl" => "application/x-pkcs7-crl",
"css" => "text/css",
"dms" => "application/octet-stream",
"doc" => "application/msword",
"dvi" => "application/x-dvi",
"eps" => "application/postscript",
"etx" => "text/x-setext",
"exe" => "application/octet-stream",
"gif" => "image/gif",
"htm" => "text/html",
"html" => "text/html",
"jpe" => "image/jpeg",
"jpeg" => "image/jpeg",
"jpg" => "image/jpeg",
"lha" => "application/octet-stream",
"lzh" => "application/octet-stream",
"mov" => "video/quicktime",
"mpe" => "video/mpeg",
"mpeg" => "video/mpeg",
"mpg" => "video/mpeg",
"pbm" => "image/x-portable-bitmap",
"pdf" => "application/pdf",
"pgm" => "image/x-portable-graymap",
"png" => "image/png",
"pnm" => "image/x-portable-anymap",
"ppm" => "image/x-portable-pixmap",
"ppt" => "application/vnd.ms-powerpoint",
"ps" => "application/postscript",
"qt" => "video/quicktime",
"ras" => "image/x-cmu-raster",
"rb" => "text/plain",
"rd" => "text/plain",
"rtf" => "application/rtf",
"sgm" => "text/sgml",
"sgml" => "text/sgml",
"tif" => "image/tiff",
"tiff" => "image/tiff",
"txt" => "text/plain",
"xbm" => "image/x-xbitmap",
"xls" => "application/vnd.ms-excel",
"xml" => "text/xml",
"xpm" => "image/x-xpixmap",
"xwd" => "image/x-xwindowdump",
"zip" => "application/zip",
}
# Load Apache compatible mime.types file.
def load_mime_types(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
def mime_type(filename, mime_tab)
if suffix = (/\.(\w+)$/ =~ filename && $1)
mtype = mime_tab[suffix.downcase]
end
mtype || "application/octet-stream"
end
module_function :mime_type
#####
def parse_header(raw)
header = Hash.new([].freeze)
field = nil
raw.each{|line|
case line
when /^([A-Za-z0-9_\-]+):\s*(.*?)\s*\z/om
field, value = $1, $2
field.downcase!
header[field] = [] unless header.has_key?(field)
header[field] << value
when /^\s+(.*?)\s*\z/om
value = $1
unless field
raise "bad header '#{line.inspect}'."
end
header[field][-1] << " " << value
else
raise "bad header '#{line.inspect}'."
end
}
header.each{|key, values|
values.each{|value|
value.strip!
value.gsub!(/\s+/, " ")
}
}
header
end
module_function :parse_header
def split_header_value(str)
str.scan(/((?:"(?:\\.|[^"])+?"|[^",]+)+)
(?:,\s*|\Z)/xn).collect{|v| v[0] }
end
module_function :split_header_value
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
#####
def dequote(str)
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
ret.gsub!(/\\(.)/, "\\1")
ret
end
module_function :dequote
def quote(str)
'"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
end
module_function :quote
#####
class FormData < String
EmptyRawHeader = [].freeze
EmptyHeader = {}.freeze
attr_accessor :name, :filename, :next_data
protected :next_data
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
def [](*key)
begin
@header[key[0].downcase].join(", ")
rescue StandardError, NameError
super
end
end
def <<(str)
if @header
super
elsif str == CRLF
@header = HTTPUtils::parse_header(@raw_header)
if cd = self['content-disposition']
if /\s+name="(.*?)"/ =~ cd then @name = $1 end
if /\s+filename="(.*?)"/ =~ cd then @filename = $1 end
end
else
@raw_header << str
end
self
end
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
def each_data
tmp = self
while tmp
next_data = tmp.next_data
yield(tmp)
tmp = next_data
end
end
def list
ret = []
each_data{|data|
data.next_data = nil
ret << data
}
ret
end
alias :to_ary :list
def to_s
String.new(self)
end
end
def parse_query(str)
query = Hash.new
if str
str.split(/[&;]/).each{|x|
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
def parse_form_data(io, boundary)
boundary_regexp = /\A--#{boundary}(--)?#{CRLF}\z/
form_data = Hash.new
data = nil
io.each{|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
def _make_regex(str) /([#{Regexp.escape(str)}])/n end
def _escape(str, regex) str.gsub(regex){ "%%%02X" % $1[0] } end
def _unescape(str, regex) str.gsub(regex){ $1.hex.chr } end
module_function :_make_regex, :_escape, :_unescape
UNESCAPED = _make_regex(control+delims+unwise+nonascii)
UNESCAPED_FORM = _make_regex(reserved+control+delims+unwise+nonascii)
NONASCII = _make_regex(nonascii)
ESCAPED = /%([0-9a-fA-F]{2})/
def escape(str)
_escape(str, UNESCAPED)
end
def unescape(str)
_unescape(str, ESCAPED)
end
def escape_form(str)
ret = _escape(str, UNESCAPED_FORM)
ret.gsub!(/ /, "+")
ret
end
def unescape_form(str)
_unescape(str.gsub(/\+/, " "), ESCAPED)
end
def escape8bit(str)
_escape(str, NONASCII)
end
module_function :escape, :unescape, :escape_form, :unescape_form,
:escape8bit
end
end

View file

@ -0,0 +1,49 @@
#
# 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
class HTTPVersion
include Comparable
attr_accessor :major, :minor
def self.convert(version)
version.is_a?(self) ? version : new(version)
end
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
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
def to_s
format("%d.%d", @major, @minor)
end
end
end

83
lib/webrick/log.rb Normal file
View file

@ -0,0 +1,83 @@
#
# 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
class BasicLog
# log-level constant
FATAL, ERROR, WARN, INFO, DEBUG = 1, 2, 3, 4, 5
attr_accessor :level
def initialize(log_file=nil, level=nil)
@level = level || INFO
case log_file
when String
@log = open(log_file, "a+")
@log.sync = true
@opened = true
when NilClass
@log = $stderr
else
@log = log_file # requires "<<". (see BasicLog#log)
end
end
def close
@log.close if @opened
@log = nil
end
def log(level, data)
if @log && level <= @level
@log << (data + "\n")
end
end
def fatal(msg) log(FATAL, "FATAL " << format(msg)); end
def error(msg) log(ERROR, "ERROR " << format(msg)); end
def warn(msg) log(WARN, "WARN " << format(msg)); end
def info(msg) log(INFO, "INFO " << format(msg)); end
def debug(msg) log(DEBUG, "DEBUG " << format(msg)); end
def fatal?; @level >= FATAL; end
def error?; @level >= ERROR; end
def warn?; @level >= WARN; end
def info?; @level >= INFO; end
def debug?; @level >= DEBUG; end
private
def format(arg)
str = if arg.is_a?(Exception)
"#{arg.class}: #{arg.message}\n\t" <<
arg.backtrace.join("\n\t")
elsif arg.respond_to?(:to_str)
arg.to_str
else
arg.inspect
end
end
end
class Log < BasicLog
attr_accessor :time_format
def initialize(log_file=nil, level=nil)
super(log_file, level)
@time_format = "[%Y-%m-%d %H:%M:%S]"
end
def log(level, data)
tmp = Time.now.strftime(@time_format)
tmp << " " << data
super(level, tmp)
end
end
end

189
lib/webrick/server.rb Normal file
View file

@ -0,0 +1,189 @@
#
# 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 'thread'
require 'socket'
require 'timeout'
require 'webrick/config'
require 'webrick/log'
module WEBrick
class ServerError < StandardError; end
class SimpleServer
def SimpleServer.start
yield
end
end
class Daemon
def Daemon.start
exit!(0) if fork
Process::setsid
exit!(0) if fork
Dir::chdir("/")
File::umask(0)
[ STDIN, STDOUT, STDERR ].each{|io|
io.reopen("/dev/null", "r+")
}
yield if block_given?
end
end
class GenericServer
attr_reader :status, :config, :logger, :tokens, :listeners
def initialize(config={}, default=Config::General)
@config = default.dup.update(config)
@status = :Stop
@config[:Logger] ||= Log::new
@logger = @config[:Logger]
@tokens = 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}")
if @config[:DoNotListen]
@listeners = []
else
@listeners = listen(@config[:BindAddress], @config[:Port])
@config[:Listen].each{|addr, port|
listen(addr, port).each{|sock| @listeners << sock }
}
end
end
def [](key)
@config[key]
end
def listen(address, port)
res = Socket::getaddrinfo(address, port,
Socket::AF_UNSPEC, # address family
Socket::SOCK_STREAM, # socket type
0, # protocol
Socket::AI_PASSIVE) # flag
last_error = nil
sockets = []
res.each{|ai|
begin
@logger.debug("TCPServer.new(#{ai[3]}, #{ai[1]})")
sock = TCPServer.new(ai[3], ai[1])
Utils::set_close_on_exec(sock)
sockets << sock
rescue => ex
@logger.warn("TCPServer Error: #{ex}")
last_error = ex
end
}
raise last_error if sockets.empty?
return sockets
end
def start(&block)
raise ServerError, "already started." if @status != :Stop
server_type = @config[:ServerType] || SimpleServer
server_type.start{
@logger.info \
"#{self.class}#start: pid=#{$$} port=#{@config[:Port]}"
call_callback(:StartCallback)
thgroup = ThreadGroup.new
@status = :Running
while @status == :Running
begin
if svrs = IO.select(@listeners, nil, nil, 2.0)
svrs[0].each{|svr|
@tokens.pop # blocks while no token is there.
sock = svr.accept
sock.sync = true
Utils::set_close_on_exec(sock)
th = start_thread(sock, &block)
th[:WEBrickThread] = true
thgroup.add(th)
}
end
rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPROTO => ex
msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
@logger.error msg
rescue Errno::EBADF => ex # IO::select causes by shutdown
rescue => ex
@logger.error ex
break
end
end
@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
def stop
if @status == :Running
@status = :Shutdown
end
end
def shutdown
stop
@listeners.each{|s|
if @logger.debug?
addr = s.addr
@logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})")
end
s.close
}
@listeners.clear
end
def run(sock)
@logger.fatal "run() must be provided by user."
end
private
def start_thread(sock, &block)
Thread.start{
begin
Thread.current[:WEBrickSocket] = sock
addr = sock.peeraddr
@logger.debug "accept: #{addr[3]}:#{addr[1]}"
call_callback(:AcceptCallback, sock)
block ? block.call(sock) : run(sock)
rescue ServerError, Errno::ENOTCONN => ex
msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
@logger.error msg
rescue Exception => ex
@logger.error ex
ensure
Thread.current[:WEBrickSocket] = nil
@logger.debug "close: #{addr[3]}:#{addr[1]}"
sock.close
end
@tokens.push(nil)
}
end
def call_callback(callback_name, *args)
if cb = @config[callback_name]
cb.call(*args)
end
end
end # end of GenericServer
end

64
lib/webrick/utils.rb Normal file
View file

@ -0,0 +1,64 @@
#
# 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 'fcntl'
begin
require 'etc'
rescue LoadError
nil
end
module WEBrick
module Utils
def set_close_on_exec(io)
if defined?(Fcntl::FD_CLOEXEC)
io.fcntl(Fcntl::FD_CLOEXEC, 1)
end
end
module_function :set_close_on_exec
def su(user, group=nil)
if defined?(Etc)
pw = Etc.getpwnam(user)
gr = group ? Etc.getgrnam(group) : pw
Process::gid = gr.gid
Process::egid = gr.gid
Process::uid = pw.uid
Process::euid = pw.uid
end
end
module_function :su
def getservername
host = Socket::gethostname
begin
Socket::gethostbyname(host)[0]
rescue
host
end
end
module_function :getservername
RAND_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789" +
"abcdefghijklmnopqrstuvwxyz"
def random_string(len)
rand_max = RAND_CHARS.size
ret = ""
len.times{ ret << RAND_CHARS[rand(rand_max)] }
ret
end
module_function :random_string
end
end

13
lib/webrick/version.rb Normal file
View file

@ -0,0 +1,13 @@
#
# 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
VERSION = "1.3.1"
end