rubysl-webrick 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE +25 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/rubysl/webrick.rb +2 -0
- data/lib/rubysl/webrick/version.rb +5 -0
- data/lib/rubysl/webrick/webrick.rb +29 -0
- data/lib/webrick.rb +1 -0
- data/lib/webrick/accesslog.rb +67 -0
- data/lib/webrick/cgi.rb +257 -0
- data/lib/webrick/compat.rb +15 -0
- data/lib/webrick/config.rb +97 -0
- data/lib/webrick/cookie.rb +110 -0
- data/lib/webrick/htmlutils.rb +25 -0
- data/lib/webrick/httpauth.rb +45 -0
- data/lib/webrick/httpauth/authenticator.rb +79 -0
- data/lib/webrick/httpauth/basicauth.rb +65 -0
- data/lib/webrick/httpauth/digestauth.rb +343 -0
- data/lib/webrick/httpauth/htdigest.rb +91 -0
- data/lib/webrick/httpauth/htgroup.rb +61 -0
- data/lib/webrick/httpauth/htpasswd.rb +83 -0
- data/lib/webrick/httpauth/userdb.rb +29 -0
- data/lib/webrick/httpproxy.rb +254 -0
- data/lib/webrick/httprequest.rb +365 -0
- data/lib/webrick/httpresponse.rb +327 -0
- data/lib/webrick/https.rb +63 -0
- data/lib/webrick/httpserver.rb +210 -0
- data/lib/webrick/httpservlet.rb +22 -0
- data/lib/webrick/httpservlet/abstract.rb +71 -0
- data/lib/webrick/httpservlet/cgi_runner.rb +45 -0
- data/lib/webrick/httpservlet/cgihandler.rb +104 -0
- data/lib/webrick/httpservlet/erbhandler.rb +54 -0
- data/lib/webrick/httpservlet/filehandler.rb +398 -0
- data/lib/webrick/httpservlet/prochandler.rb +33 -0
- data/lib/webrick/httpstatus.rb +126 -0
- data/lib/webrick/httputils.rb +391 -0
- data/lib/webrick/httpversion.rb +49 -0
- data/lib/webrick/log.rb +88 -0
- data/lib/webrick/server.rb +200 -0
- data/lib/webrick/ssl.rb +126 -0
- data/lib/webrick/utils.rb +100 -0
- data/lib/webrick/version.rb +13 -0
- data/rubysl-webrick.gemspec +23 -0
- metadata +145 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
#
|
2
|
+
# compat.rb -- cross platform compatibility
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2002 GOTOU Yuuzou
|
6
|
+
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
|
7
|
+
# reserved.
|
8
|
+
#
|
9
|
+
# $IPR: compat.rb,v 1.6 2002/10/01 17:16:32 gotoyuzo Exp $
|
10
|
+
|
11
|
+
module Errno
|
12
|
+
class EPROTO < SystemCallError; end
|
13
|
+
class ECONNRESET < SystemCallError; end
|
14
|
+
class ECONNABORTED < SystemCallError; end
|
15
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
#
|
2
|
+
# config.rb -- Default configurations.
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
|
6
|
+
# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
|
7
|
+
# reserved.
|
8
|
+
#
|
9
|
+
# $IPR: config.rb,v 1.52 2003/07/22 19:20:42 gotoyuzo Exp $
|
10
|
+
|
11
|
+
require 'webrick/version'
|
12
|
+
require 'webrick/httpversion'
|
13
|
+
require 'webrick/httputils'
|
14
|
+
require 'webrick/utils'
|
15
|
+
require 'webrick/log'
|
16
|
+
|
17
|
+
module WEBrick
|
18
|
+
module Config
|
19
|
+
LIBDIR = File::dirname(__FILE__)
|
20
|
+
|
21
|
+
# for GenericServer
|
22
|
+
General = {
|
23
|
+
:ServerName => Utils::getservername,
|
24
|
+
:BindAddress => nil, # "0.0.0.0" or "::" or nil
|
25
|
+
:Port => nil, # users MUST specifiy this!!
|
26
|
+
:MaxClients => 100, # maximum number of the concurrent connections
|
27
|
+
:ServerType => nil, # default: WEBrick::SimpleServer
|
28
|
+
:Logger => nil, # default: WEBrick::Log.new
|
29
|
+
:ServerSoftware => "WEBrick/#{WEBrick::VERSION} " +
|
30
|
+
"(Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})",
|
31
|
+
:TempDir => ENV['TMPDIR']||ENV['TMP']||ENV['TEMP']||'/tmp',
|
32
|
+
:DoNotListen => false,
|
33
|
+
:StartCallback => nil,
|
34
|
+
:StopCallback => nil,
|
35
|
+
:AcceptCallback => nil,
|
36
|
+
}
|
37
|
+
|
38
|
+
# for HTTPServer, HTTPRequest, HTTPResponse ...
|
39
|
+
HTTP = General.dup.update(
|
40
|
+
:Port => 80,
|
41
|
+
:RequestTimeout => 30,
|
42
|
+
:HTTPVersion => HTTPVersion.new("1.1"),
|
43
|
+
:AccessLog => nil,
|
44
|
+
:MimeTypes => HTTPUtils::DefaultMimeTypes,
|
45
|
+
:DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"],
|
46
|
+
:DocumentRoot => nil,
|
47
|
+
:DocumentRootOptions => { :FancyIndexing => true },
|
48
|
+
:RequestHandler => nil,
|
49
|
+
:RequestCallback => nil, # alias of :RequestHandler
|
50
|
+
:ServerAlias => nil,
|
51
|
+
|
52
|
+
# for HTTPProxyServer
|
53
|
+
:ProxyAuthProc => nil,
|
54
|
+
:ProxyContentHandler => nil,
|
55
|
+
:ProxyVia => true,
|
56
|
+
:ProxyTimeout => true,
|
57
|
+
:ProxyURI => nil,
|
58
|
+
|
59
|
+
:CGIInterpreter => nil,
|
60
|
+
:CGIPathEnv => nil,
|
61
|
+
|
62
|
+
# workaround: if Request-URIs contain 8bit chars,
|
63
|
+
# they should be escaped before calling of URI::parse().
|
64
|
+
:Escape8bitURI => false
|
65
|
+
)
|
66
|
+
|
67
|
+
FileHandler = {
|
68
|
+
:NondisclosureName => [".ht*", "*~"],
|
69
|
+
:FancyIndexing => false,
|
70
|
+
:HandlerTable => {},
|
71
|
+
:HandlerCallback => nil,
|
72
|
+
:DirectoryCallback => nil,
|
73
|
+
:FileCallback => nil,
|
74
|
+
:UserDir => nil, # e.g. "public_html"
|
75
|
+
:AcceptableLanguages => [] # ["en", "ja", ... ]
|
76
|
+
}
|
77
|
+
|
78
|
+
BasicAuth = {
|
79
|
+
:AutoReloadUserDB => true,
|
80
|
+
}
|
81
|
+
|
82
|
+
DigestAuth = {
|
83
|
+
:Algorithm => 'MD5-sess', # or 'MD5'
|
84
|
+
:Domain => nil, # an array includes domain names.
|
85
|
+
:Qop => [ 'auth' ], # 'auth' or 'auth-int' or both.
|
86
|
+
:UseOpaque => true,
|
87
|
+
:UseNextNonce => false,
|
88
|
+
:CheckNc => false,
|
89
|
+
:UseAuthenticationInfoHeader => true,
|
90
|
+
:AutoReloadUserDB => true,
|
91
|
+
:NonceExpirePeriod => 30*60,
|
92
|
+
:NonceExpireDelta => 60,
|
93
|
+
:InternetExplorerHack => true,
|
94
|
+
:OperaHack => true,
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#
|
2
|
+
# cookie.rb -- Cookie class
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
|
6
|
+
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
|
7
|
+
# reserved.
|
8
|
+
#
|
9
|
+
# $IPR: cookie.rb,v 1.16 2002/09/21 12:23:35 gotoyuzo Exp $
|
10
|
+
|
11
|
+
require 'time'
|
12
|
+
require 'webrick/httputils'
|
13
|
+
|
14
|
+
module WEBrick
|
15
|
+
class Cookie
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
attr_accessor :value, :version
|
19
|
+
attr_accessor :domain, :path, :secure
|
20
|
+
attr_accessor :comment, :max_age
|
21
|
+
#attr_accessor :comment_url, :discard, :port
|
22
|
+
|
23
|
+
def initialize(name, value)
|
24
|
+
@name = name
|
25
|
+
@value = value
|
26
|
+
@version = 0 # Netscape Cookie
|
27
|
+
|
28
|
+
@domain = @path = @secure = @comment = @max_age =
|
29
|
+
@expires = @comment_url = @discard = @port = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def expires=(t)
|
33
|
+
@expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
def expires
|
37
|
+
@expires && Time.parse(@expires)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
ret = ""
|
42
|
+
ret << @name << "=" << @value
|
43
|
+
ret << "; " << "Version=" << @version.to_s if @version > 0
|
44
|
+
ret << "; " << "Domain=" << @domain if @domain
|
45
|
+
ret << "; " << "Expires=" << @expires if @expires
|
46
|
+
ret << "; " << "Max-Age=" << @max_age.to_s if @max_age
|
47
|
+
ret << "; " << "Comment=" << @comment if @comment
|
48
|
+
ret << "; " << "Path=" << @path if @path
|
49
|
+
ret << "; " << "Secure" if @secure
|
50
|
+
ret
|
51
|
+
end
|
52
|
+
|
53
|
+
# Cookie::parse()
|
54
|
+
# It parses Cookie field sent from the user agent.
|
55
|
+
def self.parse(str)
|
56
|
+
if str
|
57
|
+
ret = []
|
58
|
+
cookie = nil
|
59
|
+
ver = 0
|
60
|
+
str.split(/[;,]\s+/).each{|x|
|
61
|
+
key, val = x.split(/=/,2)
|
62
|
+
val = val ? HTTPUtils::dequote(val) : ""
|
63
|
+
case key
|
64
|
+
when "$Version"; ver = val.to_i
|
65
|
+
when "$Path"; cookie.path = val
|
66
|
+
when "$Domain"; cookie.domain = val
|
67
|
+
when "$Port"; cookie.port = val
|
68
|
+
else
|
69
|
+
ret << cookie if cookie
|
70
|
+
cookie = self.new(key, val)
|
71
|
+
cookie.version = ver
|
72
|
+
end
|
73
|
+
}
|
74
|
+
ret << cookie if cookie
|
75
|
+
ret
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.parse_set_cookie(str)
|
80
|
+
cookie_elem = str.split(/;/)
|
81
|
+
first_elem = cookie_elem.shift
|
82
|
+
first_elem.strip!
|
83
|
+
key, value = first_elem.split(/=/, 2)
|
84
|
+
cookie = new(key, HTTPUtils.dequote(value))
|
85
|
+
cookie_elem.each{|pair|
|
86
|
+
pair.strip!
|
87
|
+
key, value = pair.split(/=/, 2)
|
88
|
+
if value
|
89
|
+
value = HTTPUtils.dequote(value.strip)
|
90
|
+
end
|
91
|
+
case key.downcase
|
92
|
+
when "domain" then cookie.domain = value
|
93
|
+
when "path" then cookie.path = value
|
94
|
+
when "expires" then cookie.expires = value
|
95
|
+
when "max-age" then cookie.max_age = Integer(value)
|
96
|
+
when "comment" then cookie.comment = value
|
97
|
+
when "version" then cookie.version = Integer(value)
|
98
|
+
when "secure" then cookie.secure = true
|
99
|
+
end
|
100
|
+
}
|
101
|
+
return cookie
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.parse_set_cookies(str)
|
105
|
+
return str.split(/,(?=[^;,]*=)|,$/).collect{|c|
|
106
|
+
parse_set_cookie(c)
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# htmlutils.rb -- HTMLUtils Module
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
|
6
|
+
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
|
7
|
+
# reserved.
|
8
|
+
#
|
9
|
+
# $IPR: htmlutils.rb,v 1.7 2002/09/21 12:23:35 gotoyuzo Exp $
|
10
|
+
|
11
|
+
module WEBrick
|
12
|
+
module HTMLUtils
|
13
|
+
|
14
|
+
def escape(string)
|
15
|
+
str = string ? string.dup : ""
|
16
|
+
str.gsub!(/&/n, '&')
|
17
|
+
str.gsub!(/\"/n, '"')
|
18
|
+
str.gsub!(/>/n, '>')
|
19
|
+
str.gsub!(/</n, '<')
|
20
|
+
str
|
21
|
+
end
|
22
|
+
module_function :escape
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#
|
2
|
+
# httpauth.rb -- HTTP access authentication
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
|
6
|
+
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
|
7
|
+
# reserved.
|
8
|
+
#
|
9
|
+
# $IPR: httpauth.rb,v 1.14 2003/07/22 19:20:42 gotoyuzo Exp $
|
10
|
+
|
11
|
+
require 'webrick/httpauth/basicauth'
|
12
|
+
require 'webrick/httpauth/digestauth'
|
13
|
+
require 'webrick/httpauth/htpasswd'
|
14
|
+
require 'webrick/httpauth/htdigest'
|
15
|
+
require 'webrick/httpauth/htgroup'
|
16
|
+
|
17
|
+
module WEBrick
|
18
|
+
module HTTPAuth
|
19
|
+
module_function
|
20
|
+
|
21
|
+
def _basic_auth(req, res, realm, req_field, res_field, err_type, block)
|
22
|
+
user = pass = nil
|
23
|
+
if /^Basic\s+(.*)/o =~ req[req_field]
|
24
|
+
userpass = $1
|
25
|
+
user, pass = userpass.unpack("m*")[0].split(":", 2)
|
26
|
+
end
|
27
|
+
if block.call(user, pass)
|
28
|
+
req.user = user
|
29
|
+
return
|
30
|
+
end
|
31
|
+
res[res_field] = "Basic realm=\"#{realm}\""
|
32
|
+
raise err_type
|
33
|
+
end
|
34
|
+
|
35
|
+
def basic_auth(req, res, realm, &block)
|
36
|
+
_basic_auth(req, res, realm, "Authorization", "WWW-Authenticate",
|
37
|
+
HTTPStatus::Unauthorized, block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def proxy_basic_auth(req, res, realm, &block)
|
41
|
+
_basic_auth(req, res, realm, "Proxy-Authorization", "Proxy-Authenticate",
|
42
|
+
HTTPStatus::ProxyAuthenticationRequired, block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
#
|
2
|
+
# httpauth/authenticator.rb -- Authenticator mix-in module.
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
|
6
|
+
# reserved.
|
7
|
+
#
|
8
|
+
# $IPR: authenticator.rb,v 1.3 2003/02/20 07:15:47 gotoyuzo Exp $
|
9
|
+
|
10
|
+
module WEBrick
|
11
|
+
module HTTPAuth
|
12
|
+
module Authenticator
|
13
|
+
RequestField = "Authorization"
|
14
|
+
ResponseField = "WWW-Authenticate"
|
15
|
+
ResponseInfoField = "Authentication-Info"
|
16
|
+
AuthException = HTTPStatus::Unauthorized
|
17
|
+
AuthScheme = nil # must override by the derived class
|
18
|
+
|
19
|
+
attr_reader :realm, :userdb, :logger
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def check_init(config)
|
24
|
+
[:UserDB, :Realm].each{|sym|
|
25
|
+
unless config[sym]
|
26
|
+
raise ArgumentError, "Argument #{sym.inspect} missing."
|
27
|
+
end
|
28
|
+
}
|
29
|
+
@realm = config[:Realm]
|
30
|
+
@userdb = config[:UserDB]
|
31
|
+
@logger = config[:Logger] || Log::new($stderr)
|
32
|
+
@reload_db = config[:AutoReloadUserDB]
|
33
|
+
@request_field = self::class::RequestField
|
34
|
+
@response_field = self::class::ResponseField
|
35
|
+
@resp_info_field = self::class::ResponseInfoField
|
36
|
+
@auth_exception = self::class::AuthException
|
37
|
+
@auth_scheme = self::class::AuthScheme
|
38
|
+
end
|
39
|
+
|
40
|
+
def check_scheme(req)
|
41
|
+
unless credentials = req[@request_field]
|
42
|
+
error("no credentials in the request.")
|
43
|
+
return nil
|
44
|
+
end
|
45
|
+
unless match = /^#{@auth_scheme}\s+/.match(credentials)
|
46
|
+
error("invalid scheme in %s.", credentials)
|
47
|
+
info("%s: %s", @request_field, credentials) if $DEBUG
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
return match.post_match
|
51
|
+
end
|
52
|
+
|
53
|
+
def log(meth, fmt, *args)
|
54
|
+
msg = format("%s %s: ", @auth_scheme, @realm)
|
55
|
+
msg << fmt % args
|
56
|
+
@logger.send(meth, msg)
|
57
|
+
end
|
58
|
+
|
59
|
+
def error(fmt, *args)
|
60
|
+
if @logger.error?
|
61
|
+
log(:error, fmt, *args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def info(fmt, *args)
|
66
|
+
if @logger.info?
|
67
|
+
log(:info, fmt, *args)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module ProxyAuthenticator
|
73
|
+
RequestField = "Proxy-Authorization"
|
74
|
+
ResponseField = "Proxy-Authenticate"
|
75
|
+
InfoField = "Proxy-Authentication-Info"
|
76
|
+
AuthException = HTTPStatus::ProxyAuthenticationRequired
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#
|
2
|
+
# httpauth/basicauth.rb -- HTTP basic access authentication
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
|
6
|
+
# reserved.
|
7
|
+
#
|
8
|
+
# $IPR: basicauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
|
9
|
+
|
10
|
+
require 'webrick/config'
|
11
|
+
require 'webrick/httpstatus'
|
12
|
+
require 'webrick/httpauth/authenticator'
|
13
|
+
|
14
|
+
module WEBrick
|
15
|
+
module HTTPAuth
|
16
|
+
class BasicAuth
|
17
|
+
include Authenticator
|
18
|
+
|
19
|
+
AuthScheme = "Basic"
|
20
|
+
|
21
|
+
def self.make_passwd(realm, user, pass)
|
22
|
+
pass ||= ""
|
23
|
+
pass.crypt(Utils::random_string(2))
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :realm, :userdb, :logger
|
27
|
+
|
28
|
+
def initialize(config, default=Config::BasicAuth)
|
29
|
+
check_init(config)
|
30
|
+
@config = default.dup.update(config)
|
31
|
+
end
|
32
|
+
|
33
|
+
def authenticate(req, res)
|
34
|
+
unless basic_credentials = check_scheme(req)
|
35
|
+
challenge(req, res)
|
36
|
+
end
|
37
|
+
userid, password = basic_credentials.unpack("m*")[0].split(":", 2)
|
38
|
+
password ||= ""
|
39
|
+
if userid.empty?
|
40
|
+
error("user id was not given.")
|
41
|
+
challenge(req, res)
|
42
|
+
end
|
43
|
+
unless encpass = @userdb.get_passwd(@realm, userid, @reload_db)
|
44
|
+
error("%s: the user is not allowed.", userid)
|
45
|
+
challenge(req, res)
|
46
|
+
end
|
47
|
+
if password.crypt(encpass) != encpass
|
48
|
+
error("%s: password unmatch.", userid)
|
49
|
+
challenge(req, res)
|
50
|
+
end
|
51
|
+
info("%s: authentication succeeded.", userid)
|
52
|
+
req.user = userid
|
53
|
+
end
|
54
|
+
|
55
|
+
def challenge(req, res)
|
56
|
+
res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\""
|
57
|
+
raise @auth_exception
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ProxyBasicAuth < BasicAuth
|
62
|
+
include ProxyAuthenticator
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,343 @@
|
|
1
|
+
#
|
2
|
+
# httpauth/digestauth.rb -- HTTP digest access authentication
|
3
|
+
#
|
4
|
+
# Author: IPR -- Internet Programming with Ruby -- writers
|
5
|
+
# Copyright (c) 2003 Internet Programming with Ruby writers.
|
6
|
+
# Copyright (c) 2003 H.M.
|
7
|
+
#
|
8
|
+
# The original implementation is provided by H.M.
|
9
|
+
# URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name=
|
10
|
+
# %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB
|
11
|
+
#
|
12
|
+
# $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
|
13
|
+
|
14
|
+
require 'webrick/config'
|
15
|
+
require 'webrick/httpstatus'
|
16
|
+
require 'webrick/httpauth/authenticator'
|
17
|
+
require 'digest/md5'
|
18
|
+
require 'digest/sha1'
|
19
|
+
|
20
|
+
module WEBrick
|
21
|
+
module HTTPAuth
|
22
|
+
class DigestAuth
|
23
|
+
include Authenticator
|
24
|
+
|
25
|
+
AuthScheme = "Digest"
|
26
|
+
OpaqueInfo = Struct.new(:time, :nonce, :nc)
|
27
|
+
attr_reader :algorithm, :qop
|
28
|
+
|
29
|
+
def self.make_passwd(realm, user, pass)
|
30
|
+
pass ||= ""
|
31
|
+
Digest::MD5::hexdigest([user, realm, pass].join(":"))
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(config, default=Config::DigestAuth)
|
35
|
+
check_init(config)
|
36
|
+
@config = default.dup.update(config)
|
37
|
+
@algorithm = @config[:Algorithm]
|
38
|
+
@domain = @config[:Domain]
|
39
|
+
@qop = @config[:Qop]
|
40
|
+
@use_opaque = @config[:UseOpaque]
|
41
|
+
@use_next_nonce = @config[:UseNextNonce]
|
42
|
+
@check_nc = @config[:CheckNc]
|
43
|
+
@use_auth_info_header = @config[:UseAuthenticationInfoHeader]
|
44
|
+
@nonce_expire_period = @config[:NonceExpirePeriod]
|
45
|
+
@nonce_expire_delta = @config[:NonceExpireDelta]
|
46
|
+
@internet_explorer_hack = @config[:InternetExplorerHack]
|
47
|
+
@opera_hack = @config[:OperaHack]
|
48
|
+
|
49
|
+
case @algorithm
|
50
|
+
when 'MD5','MD5-sess'
|
51
|
+
@h = Digest::MD5
|
52
|
+
when 'SHA1','SHA1-sess' # it is a bonus feature :-)
|
53
|
+
@h = Digest::SHA1
|
54
|
+
else
|
55
|
+
msg = format('Alogrithm "%s" is not supported.', @algorithm)
|
56
|
+
raise ArgumentError.new(msg)
|
57
|
+
end
|
58
|
+
|
59
|
+
@instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid)
|
60
|
+
@opaques = {}
|
61
|
+
@last_nonce_expire = Time.now
|
62
|
+
@mutex = Mutex.new
|
63
|
+
end
|
64
|
+
|
65
|
+
def authenticate(req, res)
|
66
|
+
unless result = @mutex.synchronize{ _authenticate(req, res) }
|
67
|
+
challenge(req, res)
|
68
|
+
end
|
69
|
+
if result == :nonce_is_stale
|
70
|
+
challenge(req, res, true)
|
71
|
+
end
|
72
|
+
return true
|
73
|
+
end
|
74
|
+
|
75
|
+
def challenge(req, res, stale=false)
|
76
|
+
nonce = generate_next_nonce(req)
|
77
|
+
if @use_opaque
|
78
|
+
opaque = generate_opaque(req)
|
79
|
+
@opaques[opaque].nonce = nonce
|
80
|
+
end
|
81
|
+
|
82
|
+
param = Hash.new
|
83
|
+
param["realm"] = HTTPUtils::quote(@realm)
|
84
|
+
param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain
|
85
|
+
param["nonce"] = HTTPUtils::quote(nonce)
|
86
|
+
param["opaque"] = HTTPUtils::quote(opaque) if opaque
|
87
|
+
param["stale"] = stale.to_s
|
88
|
+
param["algorithm"] = @algorithm
|
89
|
+
param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop
|
90
|
+
|
91
|
+
res[@response_field] =
|
92
|
+
"#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ")
|
93
|
+
info("%s: %s", @response_field, res[@response_field]) if $DEBUG
|
94
|
+
raise @auth_exception
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
MustParams = ['username','realm','nonce','uri','response']
|
100
|
+
MustParamsAuth = ['cnonce','nc']
|
101
|
+
|
102
|
+
def _authenticate(req, res)
|
103
|
+
unless digest_credentials = check_scheme(req)
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
|
107
|
+
auth_req = split_param_value(digest_credentials)
|
108
|
+
if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
109
|
+
req_params = MustParams + MustParamsAuth
|
110
|
+
else
|
111
|
+
req_params = MustParams
|
112
|
+
end
|
113
|
+
req_params.each{|key|
|
114
|
+
unless auth_req.has_key?(key)
|
115
|
+
error('%s: parameter missing. "%s"', auth_req['username'], key)
|
116
|
+
raise HTTPStatus::BadRequest
|
117
|
+
end
|
118
|
+
}
|
119
|
+
|
120
|
+
if !check_uri(req, auth_req)
|
121
|
+
raise HTTPStatus::BadRequest
|
122
|
+
end
|
123
|
+
|
124
|
+
if auth_req['realm'] != @realm
|
125
|
+
error('%s: realm unmatch. "%s" for "%s"',
|
126
|
+
auth_req['username'], auth_req['realm'], @realm)
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
|
130
|
+
auth_req['algorithm'] ||= 'MD5'
|
131
|
+
if auth_req['algorithm'] != @algorithm &&
|
132
|
+
(@opera_hack && auth_req['algorithm'] != @algorithm.upcase)
|
133
|
+
error('%s: algorithm unmatch. "%s" for "%s"',
|
134
|
+
auth_req['username'], auth_req['algorithm'], @algorithm)
|
135
|
+
return false
|
136
|
+
end
|
137
|
+
|
138
|
+
if (@qop.nil? && auth_req.has_key?('qop')) ||
|
139
|
+
(@qop && (! @qop.member?(auth_req['qop'])))
|
140
|
+
error('%s: the qop is not allowed. "%s"',
|
141
|
+
auth_req['username'], auth_req['qop'])
|
142
|
+
return false
|
143
|
+
end
|
144
|
+
|
145
|
+
password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db)
|
146
|
+
unless password
|
147
|
+
error('%s: the user is not allowd.', auth_req['username'])
|
148
|
+
return false
|
149
|
+
end
|
150
|
+
|
151
|
+
nonce_is_invalid = false
|
152
|
+
if @use_opaque
|
153
|
+
info("@opaque = %s", @opaque.inspect) if $DEBUG
|
154
|
+
if !(opaque = auth_req['opaque'])
|
155
|
+
error('%s: opaque is not given.', auth_req['username'])
|
156
|
+
nonce_is_invalid = true
|
157
|
+
elsif !(opaque_struct = @opaques[opaque])
|
158
|
+
error('%s: invalid opaque is given.', auth_req['username'])
|
159
|
+
nonce_is_invalid = true
|
160
|
+
elsif !check_opaque(opaque_struct, req, auth_req)
|
161
|
+
@opaques.delete(auth_req['opaque'])
|
162
|
+
nonce_is_invalid = true
|
163
|
+
end
|
164
|
+
elsif !check_nonce(req, auth_req)
|
165
|
+
nonce_is_invalid = true
|
166
|
+
end
|
167
|
+
|
168
|
+
if /-sess$/ =~ auth_req['algorithm'] ||
|
169
|
+
(@opera_hack && /-SESS$/ =~ auth_req['algorithm'])
|
170
|
+
ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce'])
|
171
|
+
else
|
172
|
+
ha1 = password
|
173
|
+
end
|
174
|
+
|
175
|
+
if auth_req['qop'] == "auth" || auth_req['qop'] == nil
|
176
|
+
ha2 = hexdigest(req.request_method, auth_req['uri'])
|
177
|
+
ha2_res = hexdigest("", auth_req['uri'])
|
178
|
+
elsif auth_req['qop'] == "auth-int"
|
179
|
+
ha2 = hexdigest(req.request_method, auth_req['uri'],
|
180
|
+
hexdigest(req.body))
|
181
|
+
ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body))
|
182
|
+
end
|
183
|
+
|
184
|
+
if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
185
|
+
param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key|
|
186
|
+
auth_req[key]
|
187
|
+
}.join(':')
|
188
|
+
digest = hexdigest(ha1, param2, ha2)
|
189
|
+
digest_res = hexdigest(ha1, param2, ha2_res)
|
190
|
+
else
|
191
|
+
digest = hexdigest(ha1, auth_req['nonce'], ha2)
|
192
|
+
digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res)
|
193
|
+
end
|
194
|
+
|
195
|
+
if digest != auth_req['response']
|
196
|
+
error("%s: digest unmatch.", auth_req['username'])
|
197
|
+
return false
|
198
|
+
elsif nonce_is_invalid
|
199
|
+
error('%s: digest is valid, but nonce is not valid.',
|
200
|
+
auth_req['username'])
|
201
|
+
return :nonce_is_stale
|
202
|
+
elsif @use_auth_info_header
|
203
|
+
auth_info = {
|
204
|
+
'nextnonce' => generate_next_nonce(req),
|
205
|
+
'rspauth' => digest_res
|
206
|
+
}
|
207
|
+
if @use_opaque
|
208
|
+
opaque_struct.time = req.request_time
|
209
|
+
opaque_struct.nonce = auth_info['nextnonce']
|
210
|
+
opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1)
|
211
|
+
end
|
212
|
+
if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
|
213
|
+
['qop','cnonce','nc'].each{|key|
|
214
|
+
auth_info[key] = auth_req[key]
|
215
|
+
}
|
216
|
+
end
|
217
|
+
res[@resp_info_field] = auth_info.keys.map{|key|
|
218
|
+
if key == 'nc'
|
219
|
+
key + '=' + auth_info[key]
|
220
|
+
else
|
221
|
+
key + "=" + HTTPUtils::quote(auth_info[key])
|
222
|
+
end
|
223
|
+
}.join(', ')
|
224
|
+
end
|
225
|
+
info('%s: authentication scceeded.', auth_req['username'])
|
226
|
+
req.user = auth_req['username']
|
227
|
+
return true
|
228
|
+
end
|
229
|
+
|
230
|
+
def split_param_value(string)
|
231
|
+
ret = {}
|
232
|
+
while string.size != 0
|
233
|
+
case string
|
234
|
+
when /^\s*([\w\-\.\*\%\!]+)=\s*\"((\\.|[^\"])*)\"\s*,?/
|
235
|
+
key = $1
|
236
|
+
matched = $2
|
237
|
+
string = $'
|
238
|
+
ret[key] = matched.gsub(/\\(.)/, "\\1")
|
239
|
+
when /^\s*([\w\-\.\*\%\!]+)=\s*([^,\"]*),?/
|
240
|
+
key = $1
|
241
|
+
matched = $2
|
242
|
+
string = $'
|
243
|
+
ret[key] = matched.clone
|
244
|
+
when /^s*^,/
|
245
|
+
string = $'
|
246
|
+
else
|
247
|
+
break
|
248
|
+
end
|
249
|
+
end
|
250
|
+
ret
|
251
|
+
end
|
252
|
+
|
253
|
+
def generate_next_nonce(req)
|
254
|
+
now = "%012d" % req.request_time.to_i
|
255
|
+
pk = hexdigest(now, @instance_key)[0,32]
|
256
|
+
nonce = [now + ":" + pk].pack("m*").chop # it has 60 length of chars.
|
257
|
+
nonce
|
258
|
+
end
|
259
|
+
|
260
|
+
def check_nonce(req, auth_req)
|
261
|
+
username = auth_req['username']
|
262
|
+
nonce = auth_req['nonce']
|
263
|
+
|
264
|
+
pub_time, pk = nonce.unpack("m*")[0].split(":", 2)
|
265
|
+
if (!pub_time || !pk)
|
266
|
+
error("%s: empty nonce is given", username)
|
267
|
+
return false
|
268
|
+
elsif (hexdigest(pub_time, @instance_key)[0,32] != pk)
|
269
|
+
error("%s: invalid private-key: %s for %s",
|
270
|
+
username, hexdigest(pub_time, @instance_key)[0,32], pk)
|
271
|
+
return false
|
272
|
+
end
|
273
|
+
|
274
|
+
diff_time = req.request_time.to_i - pub_time.to_i
|
275
|
+
if (diff_time < 0)
|
276
|
+
error("%s: difference of time-stamp is negative.", username)
|
277
|
+
return false
|
278
|
+
elsif diff_time > @nonce_expire_period
|
279
|
+
error("%s: nonce is expired.", username)
|
280
|
+
return false
|
281
|
+
end
|
282
|
+
|
283
|
+
return true
|
284
|
+
end
|
285
|
+
|
286
|
+
def generate_opaque(req)
|
287
|
+
@mutex.synchronize{
|
288
|
+
now = req.request_time
|
289
|
+
if now - @last_nonce_expire > @nonce_expire_delta
|
290
|
+
@opaques.delete_if{|key,val|
|
291
|
+
(now - val.time) > @nonce_expire_period
|
292
|
+
}
|
293
|
+
@last_nonce_expire = now
|
294
|
+
end
|
295
|
+
begin
|
296
|
+
opaque = Utils::random_string(16)
|
297
|
+
end while @opaques[opaque]
|
298
|
+
@opaques[opaque] = OpaqueInfo.new(now, nil, '00000001')
|
299
|
+
opaque
|
300
|
+
}
|
301
|
+
end
|
302
|
+
|
303
|
+
def check_opaque(opaque_struct, req, auth_req)
|
304
|
+
if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce)
|
305
|
+
error('%s: nonce unmatched. "%s" for "%s"',
|
306
|
+
auth_req['username'], auth_req['nonce'], opaque_struct.nonce)
|
307
|
+
return false
|
308
|
+
elsif !check_nonce(req, auth_req)
|
309
|
+
return false
|
310
|
+
end
|
311
|
+
if (@check_nc && auth_req['nc'] != opaque_struct.nc)
|
312
|
+
error('%s: nc unmatched."%s" for "%s"',
|
313
|
+
auth_req['username'], auth_req['nc'], opaque_struct.nc)
|
314
|
+
return false
|
315
|
+
end
|
316
|
+
true
|
317
|
+
end
|
318
|
+
|
319
|
+
def check_uri(req, auth_req)
|
320
|
+
uri = auth_req['uri']
|
321
|
+
if uri != req.request_uri.to_s && uri != req.unparsed_uri &&
|
322
|
+
(@internet_explorer_hack && uri != req.path)
|
323
|
+
error('%s: uri unmatch. "%s" for "%s"', auth_req['username'],
|
324
|
+
auth_req['uri'], req.request_uri.to_s)
|
325
|
+
return false
|
326
|
+
end
|
327
|
+
true
|
328
|
+
end
|
329
|
+
|
330
|
+
def hexdigest(*args)
|
331
|
+
@h.hexdigest(args.join(":"))
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
class ProxyDigestAuth < DigestAuth
|
336
|
+
include ProxyAuthenticator
|
337
|
+
|
338
|
+
def check_uri(req, auth_req)
|
339
|
+
return true
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|