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:
parent
02f036ddbc
commit
01eba908ad
33 changed files with 3881 additions and 0 deletions
29
lib/webrick.rb
Normal file
29
lib/webrick.rb
Normal 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
64
lib/webrick/accesslog.rb
Normal 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
30
lib/webrick/compat.rb
Normal 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
96
lib/webrick/config.rb
Normal 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
80
lib/webrick/cookie.rb
Normal 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
25
lib/webrick/htmlutils.rb
Normal 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, '&')
|
||||
str.gsub!(/\"/n, '"')
|
||||
str.gsub!(/>/n, '>')
|
||||
str.gsub!(/</n, '<')
|
||||
str
|
||||
end
|
||||
module_function :escape
|
||||
|
||||
end
|
||||
end
|
46
lib/webrick/httpauth.rb
Normal file
46
lib/webrick/httpauth.rb
Normal 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
|
79
lib/webrick/httpauth/authenticator.rb
Normal file
79
lib/webrick/httpauth/authenticator.rb
Normal 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
|
66
lib/webrick/httpauth/basicauth.rb
Normal file
66
lib/webrick/httpauth/basicauth.rb
Normal 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
|
348
lib/webrick/httpauth/digestauth.rb
Normal file
348
lib/webrick/httpauth/digestauth.rb
Normal 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
|
91
lib/webrick/httpauth/htdigest.rb
Normal file
91
lib/webrick/httpauth/htdigest.rb
Normal 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
|
61
lib/webrick/httpauth/htgroup.rb
Normal file
61
lib/webrick/httpauth/htgroup.rb
Normal 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
|
75
lib/webrick/httpauth/htpasswd.rb
Normal file
75
lib/webrick/httpauth/htpasswd.rb
Normal 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
|
29
lib/webrick/httpauth/userdb.rb
Normal file
29
lib/webrick/httpauth/userdb.rb
Normal 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
237
lib/webrick/httpproxy.rb
Normal 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
339
lib/webrick/httprequest.rb
Normal 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
304
lib/webrick/httpresponse.rb
Normal 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
158
lib/webrick/https.rb
Normal 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
179
lib/webrick/httpserver.rb
Normal 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
|
22
lib/webrick/httpservlet.rb
Normal file
22
lib/webrick/httpservlet.rb
Normal 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
|
71
lib/webrick/httpservlet/abstract.rb
Normal file
71
lib/webrick/httpservlet/abstract.rb
Normal 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
|
45
lib/webrick/httpservlet/cgi_runner.rb
Normal file
45
lib/webrick/httpservlet/cgi_runner.rb
Normal 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"]
|
93
lib/webrick/httpservlet/cgihandler.rb
Normal file
93
lib/webrick/httpservlet/cgihandler.rb
Normal 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
|
53
lib/webrick/httpservlet/erbhandler.rb
Normal file
53
lib/webrick/httpservlet/erbhandler.rb
Normal 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
|
330
lib/webrick/httpservlet/filehandler.rb
Normal file
330
lib/webrick/httpservlet/filehandler.rb
Normal 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
|
33
lib/webrick/httpservlet/prochandler.rb
Normal file
33
lib/webrick/httpservlet/prochandler.rb
Normal 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
126
lib/webrick/httpstatus.rb
Normal 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
374
lib/webrick/httputils.rb
Normal 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
|
49
lib/webrick/httpversion.rb
Normal file
49
lib/webrick/httpversion.rb
Normal 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
83
lib/webrick/log.rb
Normal 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
189
lib/webrick/server.rb
Normal 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
64
lib/webrick/utils.rb
Normal 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
13
lib/webrick/version.rb
Normal 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
|
Loading…
Add table
Reference in a new issue