rubysl-webrick 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|