From 01eba908adcd150a7b816af0dbe167c4c4912a90 Mon Sep 17 00:00:00 2001 From: gotoyuzo Date: Wed, 23 Jul 2003 16:51:36 +0000 Subject: * lib/webrick: imported. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@4130 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/webrick.rb | 29 +++ lib/webrick/accesslog.rb | 64 ++++++ lib/webrick/compat.rb | 30 +++ lib/webrick/config.rb | 96 +++++++++ lib/webrick/cookie.rb | 80 +++++++ lib/webrick/htmlutils.rb | 25 +++ lib/webrick/httpauth.rb | 46 ++++ lib/webrick/httpauth/authenticator.rb | 79 +++++++ lib/webrick/httpauth/basicauth.rb | 66 ++++++ lib/webrick/httpauth/digestauth.rb | 348 ++++++++++++++++++++++++++++++ lib/webrick/httpauth/htdigest.rb | 91 ++++++++ lib/webrick/httpauth/htgroup.rb | 61 ++++++ lib/webrick/httpauth/htpasswd.rb | 75 +++++++ lib/webrick/httpauth/userdb.rb | 29 +++ lib/webrick/httpproxy.rb | 237 +++++++++++++++++++++ lib/webrick/httprequest.rb | 339 ++++++++++++++++++++++++++++++ lib/webrick/httpresponse.rb | 304 +++++++++++++++++++++++++++ lib/webrick/https.rb | 158 ++++++++++++++ lib/webrick/httpserver.rb | 179 ++++++++++++++++ lib/webrick/httpservlet.rb | 22 ++ lib/webrick/httpservlet/abstract.rb | 71 +++++++ lib/webrick/httpservlet/cgi_runner.rb | 45 ++++ lib/webrick/httpservlet/cgihandler.rb | 93 ++++++++ lib/webrick/httpservlet/erbhandler.rb | 53 +++++ lib/webrick/httpservlet/filehandler.rb | 330 +++++++++++++++++++++++++++++ lib/webrick/httpservlet/prochandler.rb | 33 +++ lib/webrick/httpstatus.rb | 126 +++++++++++ lib/webrick/httputils.rb | 374 +++++++++++++++++++++++++++++++++ lib/webrick/httpversion.rb | 49 +++++ lib/webrick/log.rb | 83 ++++++++ lib/webrick/server.rb | 189 +++++++++++++++++ lib/webrick/utils.rb | 64 ++++++ lib/webrick/version.rb | 13 ++ 33 files changed, 3881 insertions(+) create mode 100644 lib/webrick.rb create mode 100644 lib/webrick/accesslog.rb create mode 100644 lib/webrick/compat.rb create mode 100644 lib/webrick/config.rb create mode 100644 lib/webrick/cookie.rb create mode 100644 lib/webrick/htmlutils.rb create mode 100644 lib/webrick/httpauth.rb create mode 100644 lib/webrick/httpauth/authenticator.rb create mode 100644 lib/webrick/httpauth/basicauth.rb create mode 100644 lib/webrick/httpauth/digestauth.rb create mode 100644 lib/webrick/httpauth/htdigest.rb create mode 100644 lib/webrick/httpauth/htgroup.rb create mode 100644 lib/webrick/httpauth/htpasswd.rb create mode 100644 lib/webrick/httpauth/userdb.rb create mode 100644 lib/webrick/httpproxy.rb create mode 100644 lib/webrick/httprequest.rb create mode 100644 lib/webrick/httpresponse.rb create mode 100644 lib/webrick/https.rb create mode 100644 lib/webrick/httpserver.rb create mode 100644 lib/webrick/httpservlet.rb create mode 100644 lib/webrick/httpservlet/abstract.rb create mode 100644 lib/webrick/httpservlet/cgi_runner.rb create mode 100644 lib/webrick/httpservlet/cgihandler.rb create mode 100644 lib/webrick/httpservlet/erbhandler.rb create mode 100644 lib/webrick/httpservlet/filehandler.rb create mode 100644 lib/webrick/httpservlet/prochandler.rb create mode 100644 lib/webrick/httpstatus.rb create mode 100644 lib/webrick/httputils.rb create mode 100644 lib/webrick/httpversion.rb create mode 100644 lib/webrick/log.rb create mode 100644 lib/webrick/server.rb create mode 100644 lib/webrick/utils.rb create mode 100644 lib/webrick/version.rb (limited to 'lib') diff --git a/lib/webrick.rb b/lib/webrick.rb new file mode 100644 index 0000000000..8fca81bafb --- /dev/null +++ b/lib/webrick.rb @@ -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' diff --git a/lib/webrick/accesslog.rb b/lib/webrick/accesslog.rb new file mode 100644 index 0000000000..10a801196c --- /dev/null +++ b/lib/webrick/accesslog.rb @@ -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 diff --git a/lib/webrick/compat.rb b/lib/webrick/compat.rb new file mode 100644 index 0000000000..a972204ff1 --- /dev/null +++ b/lib/webrick/compat.rb @@ -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 diff --git a/lib/webrick/config.rb b/lib/webrick/config.rb new file mode 100644 index 0000000000..229beada08 --- /dev/null +++ b/lib/webrick/config.rb @@ -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 diff --git a/lib/webrick/cookie.rb b/lib/webrick/cookie.rb new file mode 100644 index 0000000000..4785b2bb33 --- /dev/null +++ b/lib/webrick/cookie.rb @@ -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 diff --git a/lib/webrick/htmlutils.rb b/lib/webrick/htmlutils.rb new file mode 100644 index 0000000000..cf8d542c09 --- /dev/null +++ b/lib/webrick/htmlutils.rb @@ -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!(/ 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 diff --git a/lib/webrick/httpauth/htdigest.rb b/lib/webrick/httpauth/htdigest.rb new file mode 100644 index 0000000000..3949756f2b --- /dev/null +++ b/lib/webrick/httpauth/htdigest.rb @@ -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 diff --git a/lib/webrick/httpauth/htgroup.rb b/lib/webrick/httpauth/htgroup.rb new file mode 100644 index 0000000000..c9270c61cc --- /dev/null +++ b/lib/webrick/httpauth/htgroup.rb @@ -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 diff --git a/lib/webrick/httpauth/htpasswd.rb b/lib/webrick/httpauth/htpasswd.rb new file mode 100644 index 0000000000..a4a80647d8 --- /dev/null +++ b/lib/webrick/httpauth/htpasswd.rb @@ -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 diff --git a/lib/webrick/httpauth/userdb.rb b/lib/webrick/httpauth/userdb.rb new file mode 100644 index 0000000000..33e01405f4 --- /dev/null +++ b/lib/webrick/httpauth/userdb.rb @@ -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 diff --git a/lib/webrick/httpproxy.rb b/lib/webrick/httpproxy.rb new file mode 100644 index 0000000000..c3bbbc54be --- /dev/null +++ b/lib/webrick/httpproxy.rb @@ -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 diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb new file mode 100644 index 0000000000..8b0803878e --- /dev/null +++ b/lib/webrick/httprequest.rb @@ -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 diff --git a/lib/webrick/httpresponse.rb b/lib/webrick/httpresponse.rb new file mode 100644 index 0000000000..6b00c2b88b --- /dev/null +++ b/lib/webrick/httpresponse.rb @@ -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 = "#{url.to_s}.\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_ + + + #{HTMLUtils::escape(@reason_phrase)} + +

#{HTMLUtils::escape(@reason_phrase)}

+ #{HTMLUtils::escape(ex.message)} +
+ _end_of_html_ + + if backtrace && $DEBUG + @body << "backtrace of `#{HTMLUtils::escape(ex.class.to_s)}' " + @body << "#{HTMLUtils::escape(ex.message)}" + @body << "
"
+        ex.backtrace.each{|line| @body << "\t#{line}\n"}
+        @body << "

" + end + + @body << <<-_end_of_html_ +
+ #{HTMLUtils::escape(@config[:ServerSoftware])} at + #{host}:#{port} +
+ + + _end_of_html_ + end + + 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 diff --git a/lib/webrick/https.rb b/lib/webrick/https.rb new file mode 100644 index 0000000000..00fd469f1b --- /dev/null +++ b/lib/webrick/https.rb @@ -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 diff --git a/lib/webrick/httpserver.rb b/lib/webrick/httpserver.rb new file mode 100644 index 0000000000..df06e19e2c --- /dev/null +++ b/lib/webrick/httpserver.rb @@ -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 diff --git a/lib/webrick/httpservlet.rb b/lib/webrick/httpservlet.rb new file mode 100644 index 0000000000..ac7c022bd7 --- /dev/null +++ b/lib/webrick/httpservlet.rb @@ -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 diff --git a/lib/webrick/httpservlet/abstract.rb b/lib/webrick/httpservlet/abstract.rb new file mode 100644 index 0000000000..03861e8fc7 --- /dev/null +++ b/lib/webrick/httpservlet/abstract.rb @@ -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 diff --git a/lib/webrick/httpservlet/cgi_runner.rb b/lib/webrick/httpservlet/cgi_runner.rb new file mode 100644 index 0000000000..1069a68d58 --- /dev/null +++ b/lib/webrick/httpservlet/cgi_runner.rb @@ -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"] diff --git a/lib/webrick/httpservlet/cgihandler.rb b/lib/webrick/httpservlet/cgihandler.rb new file mode 100644 index 0000000000..70708610d1 --- /dev/null +++ b/lib/webrick/httpservlet/cgihandler.rb @@ -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 diff --git a/lib/webrick/httpservlet/erbhandler.rb b/lib/webrick/httpservlet/erbhandler.rb new file mode 100644 index 0000000000..40b7a57610 --- /dev/null +++ b/lib/webrick/httpservlet/erbhandler.rb @@ -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 diff --git a/lib/webrick/httpservlet/filehandler.rb b/lib/webrick/httpservlet/filehandler.rb new file mode 100644 index 0000000000..f6db991bf6 --- /dev/null +++ b/lib/webrick/httpservlet/filehandler.rb @@ -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_ + + + Index of #{HTMLUtils::escape(req.path)} + +

Index of #{HTMLUtils::escape(req.path)}

+ _end_of_html_ + + res.body << "
\n"
+        res.body << " Name                          "
+        res.body << "Last modified         "
+        res.body << "Size\n"
+        res.body << "
\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 = " #{dname}" + 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 << "

" + + res.body << <<-_end_of_html_ +
+ #{HTMLUtils::escape(@config[:ServerSoftware])}
+ at #{req.request_uri.host}:#{@config[:Port]} +
+ + + _end_of_html_ + end + + end + end +end diff --git a/lib/webrick/httpservlet/prochandler.rb b/lib/webrick/httpservlet/prochandler.rb new file mode 100644 index 0000000000..783cb27896 --- /dev/null +++ b/lib/webrick/httpservlet/prochandler.rb @@ -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 diff --git a/lib/webrick/httpstatus.rb b/lib/webrick/httpstatus.rb new file mode 100644 index 0000000000..0b22c992b3 --- /dev/null +++ b/lib/webrick/httpstatus.rb @@ -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 diff --git a/lib/webrick/httputils.rb b/lib/webrick/httputils.rb new file mode 100644 index 0000000000..ce4defbb28 --- /dev/null +++ b/lib/webrick/httputils.rb @@ -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 diff --git a/lib/webrick/httpversion.rb b/lib/webrick/httpversion.rb new file mode 100644 index 0000000000..86907a26bd --- /dev/null +++ b/lib/webrick/httpversion.rb @@ -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 diff --git a/lib/webrick/log.rb b/lib/webrick/log.rb new file mode 100644 index 0000000000..2f56102736 --- /dev/null +++ b/lib/webrick/log.rb @@ -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 diff --git a/lib/webrick/server.rb b/lib/webrick/server.rb new file mode 100644 index 0000000000..911f78b66a --- /dev/null +++ b/lib/webrick/server.rb @@ -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 diff --git a/lib/webrick/utils.rb b/lib/webrick/utils.rb new file mode 100644 index 0000000000..646880d655 --- /dev/null +++ b/lib/webrick/utils.rb @@ -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 diff --git a/lib/webrick/version.rb b/lib/webrick/version.rb new file mode 100644 index 0000000000..b2b9fd3b78 --- /dev/null +++ b/lib/webrick/version.rb @@ -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 -- cgit v1.2.3