tilia-http 4.1.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 +19 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +235 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +69 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +68 -0
- data/Rakefile +17 -0
- data/examples/asyncclient.rb +45 -0
- data/examples/basicauth.rb +39 -0
- data/examples/client.rb +20 -0
- data/examples/reverseproxy.rb +39 -0
- data/examples/stringify.rb +37 -0
- data/lib/tilia/http/auth/abstract_auth.rb +51 -0
- data/lib/tilia/http/auth/aws.rb +191 -0
- data/lib/tilia/http/auth/basic.rb +43 -0
- data/lib/tilia/http/auth/bearer.rb +37 -0
- data/lib/tilia/http/auth/digest.rb +187 -0
- data/lib/tilia/http/auth.rb +12 -0
- data/lib/tilia/http/client.rb +452 -0
- data/lib/tilia/http/client_exception.rb +15 -0
- data/lib/tilia/http/client_http_exception.rb +37 -0
- data/lib/tilia/http/http_exception.rb +21 -0
- data/lib/tilia/http/message.rb +241 -0
- data/lib/tilia/http/message_decorator_trait.rb +183 -0
- data/lib/tilia/http/message_interface.rb +154 -0
- data/lib/tilia/http/request.rb +235 -0
- data/lib/tilia/http/request_decorator.rb +160 -0
- data/lib/tilia/http/request_interface.rb +126 -0
- data/lib/tilia/http/response.rb +164 -0
- data/lib/tilia/http/response_decorator.rb +58 -0
- data/lib/tilia/http/response_interface.rb +36 -0
- data/lib/tilia/http/sapi.rb +165 -0
- data/lib/tilia/http/url_util.rb +70 -0
- data/lib/tilia/http/util.rb +51 -0
- data/lib/tilia/http/version.rb +9 -0
- data/lib/tilia/http.rb +416 -0
- data/test/http/auth/aws_test.rb +189 -0
- data/test/http/auth/basic_test.rb +60 -0
- data/test/http/auth/bearer_test.rb +47 -0
- data/test/http/auth/digest_test.rb +141 -0
- data/test/http/client_mock.rb +101 -0
- data/test/http/client_test.rb +331 -0
- data/test/http/message_decorator_test.rb +67 -0
- data/test/http/message_test.rb +163 -0
- data/test/http/request_decorator_test.rb +87 -0
- data/test/http/request_test.rb +132 -0
- data/test/http/response_decorator_test.rb +28 -0
- data/test/http/response_test.rb +38 -0
- data/test/http/sapi_mock.rb +12 -0
- data/test/http/sapi_test.rb +133 -0
- data/test/http/url_util_test.rb +155 -0
- data/test/http/util_test.rb +186 -0
- data/test/http_test.rb +102 -0
- data/test/test_helper.rb +6 -0
- data/tilia-http.gemspec +18 -0
- metadata +192 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# The url we're proxying to.
|
3
|
+
remote_url = 'http://example.org/'
|
4
|
+
|
5
|
+
# The url we're proxying from. Please note that this must be a relative url,
|
6
|
+
# and basically acts as the base url.
|
7
|
+
#
|
8
|
+
# If your remote_url doesn't end with a slash, this one probably shouldn't
|
9
|
+
# either.
|
10
|
+
my_base_url = ''
|
11
|
+
# my_base_url = '/~evert/sabre/http/examples/reverseproxy.php/'
|
12
|
+
|
13
|
+
# Expected to be called "bundle exec examples/reverseproxy.rb"
|
14
|
+
require './lib/tilia_http'
|
15
|
+
require 'rack'
|
16
|
+
|
17
|
+
app = proc do |env|
|
18
|
+
sapi = Tilia::Http::Sapi.new(env)
|
19
|
+
request = sapi.request
|
20
|
+
request.base_url = my_base_url
|
21
|
+
|
22
|
+
sub_request = request.dup
|
23
|
+
|
24
|
+
# Removing the Host header.
|
25
|
+
sub_request.remove_header('Host')
|
26
|
+
|
27
|
+
# Rewriting the url.
|
28
|
+
sub_request.url = remote_url + request.path
|
29
|
+
|
30
|
+
client = Tilia::Http::Client.new
|
31
|
+
|
32
|
+
# Sends the HTTP request to the server
|
33
|
+
response = client.send(sub_request)
|
34
|
+
|
35
|
+
# Sends the response back to the client that connected to the proxy.
|
36
|
+
sapi.send_response(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
Rack::Handler::WEBrick.run app
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'json'
|
3
|
+
# This simple example shows the capability of Request and Response objects to
|
4
|
+
# serialize themselves as strings.
|
5
|
+
#
|
6
|
+
# This is mainly useful for debugging purposes.
|
7
|
+
#
|
8
|
+
# @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
|
9
|
+
# @author Evert Pot (http://evertpot.com/)
|
10
|
+
# @license http://sabre.io/license/ Modified BSD License
|
11
|
+
|
12
|
+
# Expected to be called "bundle exec examples/stringify.rb"
|
13
|
+
require './lib/tilia_http'
|
14
|
+
|
15
|
+
request = Tilia::Http::Request.new('POST', '/foo')
|
16
|
+
request.update_headers(
|
17
|
+
'Host' => 'example.org',
|
18
|
+
'Content-Type' => 'application/json'
|
19
|
+
)
|
20
|
+
|
21
|
+
request.body = JSON.generate('foo' => 'bar')
|
22
|
+
|
23
|
+
puts request.to_s
|
24
|
+
puts
|
25
|
+
puts
|
26
|
+
|
27
|
+
response = Tilia::Http::Response.new(424)
|
28
|
+
response.update_headers(
|
29
|
+
'Content-Type' => 'text/plain',
|
30
|
+
'Connection' => 'close'
|
31
|
+
)
|
32
|
+
|
33
|
+
response.body = 'ABORT! ABORT!'
|
34
|
+
|
35
|
+
puts response.to_s
|
36
|
+
|
37
|
+
puts
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Http
|
3
|
+
module Auth
|
4
|
+
# HTTP Authentication base class.
|
5
|
+
#
|
6
|
+
# This class provides some common functionality for the various base classes.
|
7
|
+
class AbstractAuth
|
8
|
+
protected
|
9
|
+
|
10
|
+
# Authentication realm
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
attr_accessor :realm
|
14
|
+
|
15
|
+
# Request object
|
16
|
+
#
|
17
|
+
# @return [RequestInterface]
|
18
|
+
attr_accessor :request
|
19
|
+
|
20
|
+
# Response object
|
21
|
+
#
|
22
|
+
# @return [ResponseInterface]
|
23
|
+
attr_accessor :response
|
24
|
+
|
25
|
+
public
|
26
|
+
|
27
|
+
# Creates the object
|
28
|
+
#
|
29
|
+
# @param [String] realm
|
30
|
+
# @return [void]
|
31
|
+
def initialize(realm = 'TiliaTooth', request, response)
|
32
|
+
@realm = realm
|
33
|
+
@request = request
|
34
|
+
@response = response
|
35
|
+
end
|
36
|
+
|
37
|
+
# This method sends the needed HTTP header and statuscode (401) to force
|
38
|
+
# the user to login.
|
39
|
+
#
|
40
|
+
# @return [void]
|
41
|
+
def require_login
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the HTTP realm
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
attr_reader :realm
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'base64'
|
3
|
+
require 'openssl'
|
4
|
+
module Tilia
|
5
|
+
module Http
|
6
|
+
module Auth
|
7
|
+
# HTTP AWS Authentication handler
|
8
|
+
#
|
9
|
+
# Use this class to leverage amazon's AWS authentication header
|
10
|
+
class Aws < Tilia::Http::Auth::AbstractAuth
|
11
|
+
protected
|
12
|
+
|
13
|
+
# The signature supplied by the HTTP client
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
attr_accessor :signature
|
17
|
+
|
18
|
+
# The accesskey supplied by the HTTP client
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
attr_accessor :access_key
|
22
|
+
|
23
|
+
public
|
24
|
+
|
25
|
+
# An error code, if any
|
26
|
+
#
|
27
|
+
# This value will be filled with one of the ERR_* constants
|
28
|
+
#
|
29
|
+
# @return int
|
30
|
+
attr_accessor :error_code
|
31
|
+
|
32
|
+
ERR_NOAWSHEADER = 1
|
33
|
+
ERR_MD5CHECKSUMWRONG = 2
|
34
|
+
ERR_INVALIDDATEFORMAT = 3
|
35
|
+
ERR_REQUESTTIMESKEWED = 4
|
36
|
+
ERR_INVALIDSIGNATURE = 5
|
37
|
+
|
38
|
+
# Gathers all information from the headers
|
39
|
+
#
|
40
|
+
# This method needs to be called prior to anything else.
|
41
|
+
#
|
42
|
+
# @return bool
|
43
|
+
def init
|
44
|
+
auth_header = @request.header('Authorization') || ''
|
45
|
+
auth_header = auth_header.split(' ')
|
46
|
+
|
47
|
+
if auth_header[0] != 'AWS' || auth_header.size < 2
|
48
|
+
@error_code = self.class::ERR_NOAWSHEADER
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
(@access_key, @signature) = auth_header[1].split(':')
|
53
|
+
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the username for the request
|
58
|
+
#
|
59
|
+
# @return [String]
|
60
|
+
attr_reader :access_key
|
61
|
+
|
62
|
+
# Validates the signature based on the secretKey
|
63
|
+
#
|
64
|
+
# @param [String] secret_key
|
65
|
+
# @return bool
|
66
|
+
def validate(secret_key)
|
67
|
+
content_md5 = @request.header('Content-MD5')
|
68
|
+
|
69
|
+
if content_md5
|
70
|
+
# We need to validate the integrity of the request
|
71
|
+
body = @request.body
|
72
|
+
@request.body = body
|
73
|
+
|
74
|
+
if content_md5 != Base64.strict_encode64(::Digest::MD5.digest(body.to_s))
|
75
|
+
# content-md5 header did not match md5 signature of body
|
76
|
+
@error_code = self.class::ERR_MD5CHECKSUMWRONG
|
77
|
+
return false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
request_date = @request.header('x-amz-date')
|
82
|
+
request_date = @request.header('Date') unless request_date
|
83
|
+
|
84
|
+
return false unless validate_rfc2616_date(request_date)
|
85
|
+
|
86
|
+
amz_headers = self.amz_headers
|
87
|
+
|
88
|
+
signature = Base64.strict_encode64(
|
89
|
+
hmacsha1(
|
90
|
+
secret_key,
|
91
|
+
@request.method + "\n" +
|
92
|
+
content_md5 + "\n" +
|
93
|
+
@request.header('Content-type').to_s + "\n" +
|
94
|
+
request_date + "\n" +
|
95
|
+
amz_headers +
|
96
|
+
@request.url
|
97
|
+
)
|
98
|
+
)
|
99
|
+
|
100
|
+
unless @signature == signature
|
101
|
+
@error_code = self.class::ERR_INVALIDSIGNATURE
|
102
|
+
return false
|
103
|
+
end
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns an HTTP 401 header, forcing login
|
108
|
+
#
|
109
|
+
# This should be called when username and password are incorrect, or not supplied at all
|
110
|
+
#
|
111
|
+
# @return [void]
|
112
|
+
def require_login
|
113
|
+
@response.add_header('WWW-Authenticate', 'AWS')
|
114
|
+
@response.status = 401
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
# Makes sure the supplied value is a valid RFC2616 date.
|
120
|
+
#
|
121
|
+
# If we would just use strtotime to get a valid timestamp, we have no way of checking if a
|
122
|
+
# user just supplied the word 'now' for the date header.
|
123
|
+
#
|
124
|
+
# This function also makes sure the Date header is within 15 minutes of the operating
|
125
|
+
# system date, to prevent replay attacks.
|
126
|
+
#
|
127
|
+
# @param [String] date_header
|
128
|
+
# @return bool
|
129
|
+
def validate_rfc2616_date(date_header)
|
130
|
+
date = Tilia::Http::Util.parse_http_date(date_header)
|
131
|
+
|
132
|
+
# Unknown format
|
133
|
+
unless date
|
134
|
+
@error_code = self.class::ERR_INVALIDDATEFORMAT
|
135
|
+
return false
|
136
|
+
end
|
137
|
+
|
138
|
+
min = Time.zone.now - 15.minutes
|
139
|
+
max = Time.zone.now + 15.minutes
|
140
|
+
|
141
|
+
# We allow 15 minutes around the current date/time
|
142
|
+
if date > max || date < min
|
143
|
+
@error_code = self.class::ERR_REQUESTTIMESKEWED
|
144
|
+
return false
|
145
|
+
end
|
146
|
+
|
147
|
+
date
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns a list of AMZ headers
|
151
|
+
#
|
152
|
+
# @return [String]
|
153
|
+
def amz_headers
|
154
|
+
amz_headers = {}
|
155
|
+
headers = @request.headers
|
156
|
+
|
157
|
+
headers.each do |header_name, header_value|
|
158
|
+
if header_name.downcase.index('x-amz-') == 0
|
159
|
+
amz_headers[header_name.downcase] = header_value[0].gsub(/\r?\n/, ' ') + "\n"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
header_str = ''
|
164
|
+
amz_headers.keys.sort.each do |h|
|
165
|
+
header_str << "#{h}:#{amz_headers[h]}"
|
166
|
+
end
|
167
|
+
|
168
|
+
header_str
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
# Generates an HMAC-SHA1 signature
|
174
|
+
#
|
175
|
+
# @param [String] key
|
176
|
+
# @param [String] message
|
177
|
+
# @return [String]
|
178
|
+
def hmacsha1(key, message)
|
179
|
+
# Built in in Ruby
|
180
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), key, message)
|
181
|
+
end
|
182
|
+
|
183
|
+
# TODO: document
|
184
|
+
def initialize(realm = 'TiliaTooth', request, response)
|
185
|
+
super
|
186
|
+
@error_code = 0
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'base64'
|
2
|
+
module Tilia
|
3
|
+
module Http
|
4
|
+
module Auth
|
5
|
+
# HTTP Basic authentication utility.
|
6
|
+
#
|
7
|
+
# This class helps you setup basic auth. The process is fairly simple:
|
8
|
+
#
|
9
|
+
# 1. Instantiate the class.
|
10
|
+
# 2. Call getCredentials (this will return null or a user/pass pair)
|
11
|
+
# 3. If you didn't get valid credentials, call 'requireLogin'
|
12
|
+
class Basic < Tilia::Http::Auth::AbstractAuth
|
13
|
+
# This method returns a numeric array with a username and password as the
|
14
|
+
# only elements.
|
15
|
+
#
|
16
|
+
# If no credentials were found, this method returns null.
|
17
|
+
#
|
18
|
+
# @return null|array
|
19
|
+
def credentials
|
20
|
+
auth = @request.header('Authorization')
|
21
|
+
|
22
|
+
return nil unless auth
|
23
|
+
return nil unless auth[0..5].downcase == 'basic '
|
24
|
+
|
25
|
+
credentials = Base64.decode64(auth[6..-1]).split(':', 2)
|
26
|
+
|
27
|
+
return nil unless credentials.size == 2
|
28
|
+
|
29
|
+
credentials
|
30
|
+
end
|
31
|
+
|
32
|
+
# This method sends the needed HTTP header and statuscode (401) to force
|
33
|
+
# the user to login.
|
34
|
+
#
|
35
|
+
# @return [void]
|
36
|
+
def require_login
|
37
|
+
@response.add_header('WWW-Authenticate', "Basic realm=\"#{@realm}\"")
|
38
|
+
@response.status = 401
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Http
|
3
|
+
module Auth
|
4
|
+
# HTTP Bearer authentication utility.
|
5
|
+
#
|
6
|
+
# This class helps you setup bearer auth. The process is fairly simple:
|
7
|
+
#
|
8
|
+
# 1. Instantiate the class.
|
9
|
+
# 2. Call getToken (this will return null or a token as string)
|
10
|
+
# 3. If you didn't get a valid token, call 'requireLogin'
|
11
|
+
class Bearer < Tilia::Http::Auth::AbstractAuth
|
12
|
+
# This method returns a string with an access token.
|
13
|
+
#
|
14
|
+
# If no token was found, this method returns null.
|
15
|
+
#
|
16
|
+
# @return null|string
|
17
|
+
def token
|
18
|
+
auth = @request.header('Authorization')
|
19
|
+
|
20
|
+
return nil unless auth
|
21
|
+
return nil unless auth[0..6].downcase == 'bearer '
|
22
|
+
|
23
|
+
auth[7..-1]
|
24
|
+
end
|
25
|
+
|
26
|
+
# This method sends the needed HTTP header and statuscode (401) to force
|
27
|
+
# authentication.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def require_login
|
31
|
+
@response.add_header('WWW-Authenticate', "Bearer realm=\"#{@realm}\"")
|
32
|
+
@response.status = 401
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'digest'
|
2
|
+
module Tilia
|
3
|
+
module Http
|
4
|
+
module Auth
|
5
|
+
# HTTP Digest Authentication handler
|
6
|
+
#
|
7
|
+
# Use this class for easy http digest authentication.
|
8
|
+
# Instructions:
|
9
|
+
#
|
10
|
+
# 1. Create the object
|
11
|
+
# 2. Call the set_realm method with the realm you plan to use
|
12
|
+
# 3. Call the init method function.
|
13
|
+
# 4. Call the user_name function. This function may return null if no
|
14
|
+
# authentication information was supplied. Based on the username you
|
15
|
+
# should check your internal database for either the associated password,
|
16
|
+
# or the so-called A1 hash of the digest.
|
17
|
+
# 5. Call either validate_password or validate_a1. This will return true
|
18
|
+
# or false.
|
19
|
+
# 6. To make sure an authentication prompt is displayed, call the
|
20
|
+
# require_login method.
|
21
|
+
class Digest < Tilia::Http::Auth::AbstractAuth
|
22
|
+
# These constants are used in qop
|
23
|
+
QOP_AUTH = 1
|
24
|
+
QOP_AUTHINT = 2
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
attr_accessor :nonce
|
29
|
+
attr_accessor :opaque
|
30
|
+
attr_accessor :digest_parts
|
31
|
+
attr_accessor :a1
|
32
|
+
attr_reader :qop
|
33
|
+
|
34
|
+
public
|
35
|
+
|
36
|
+
# Initializes the object
|
37
|
+
def initialize(realm = 'TiliaTooth', request, response)
|
38
|
+
@qop = self.class::QOP_AUTH
|
39
|
+
|
40
|
+
@nonce = ::Digest::SHA1.hexdigest((Time.now.to_f + rand).to_s)[0..14]
|
41
|
+
@opaque = ::Digest::MD5.hexdigest(realm)
|
42
|
+
super(realm, request, response)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Gathers all information from the headers
|
46
|
+
#
|
47
|
+
# This method needs to be called prior to anything else.
|
48
|
+
#
|
49
|
+
# @return [void]
|
50
|
+
def init
|
51
|
+
digest = self.digest
|
52
|
+
@digest_parts = parse_digest(digest)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sets the quality of protection value.
|
56
|
+
#
|
57
|
+
# Possible values are:
|
58
|
+
# Sabre\HTTP\DigestAuth::QOP_AUTH
|
59
|
+
# Sabre\HTTP\DigestAuth::QOP_AUTHINT
|
60
|
+
#
|
61
|
+
# Multiple values can be specified using logical OR.
|
62
|
+
#
|
63
|
+
# QOP_AUTHINT ensures integrity of the request body, but this is not
|
64
|
+
# supported by most HTTP clients. QOP_AUTHINT also requires the entire
|
65
|
+
# request body to be md5'ed, which can put strains on CPU and memory.
|
66
|
+
#
|
67
|
+
# @param int qop
|
68
|
+
# @return [void]
|
69
|
+
attr_writer :qop
|
70
|
+
|
71
|
+
# Validates the user.
|
72
|
+
#
|
73
|
+
# The A1 parameter should be md5(username . ':' . realm . ':' . password)
|
74
|
+
#
|
75
|
+
# @param [String] a1
|
76
|
+
# @return bool
|
77
|
+
def validate_a1(a1)
|
78
|
+
@a1 = a1
|
79
|
+
validate
|
80
|
+
end
|
81
|
+
|
82
|
+
# Validates authentication through a password. The actual password must be provided here.
|
83
|
+
# It is strongly recommended not store the password in plain-text and use validateA1 instead.
|
84
|
+
#
|
85
|
+
# @param [String] password
|
86
|
+
# @return bool
|
87
|
+
def validate_password(password)
|
88
|
+
return false unless @digest_parts.any? # RUBY
|
89
|
+
|
90
|
+
@a1 = ::Digest::MD5.hexdigest(@digest_parts['username'] + ':' + @realm + ':' + password)
|
91
|
+
validate
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the username for the request
|
95
|
+
#
|
96
|
+
# @return [String]
|
97
|
+
def username
|
98
|
+
@digest_parts['username']
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
# Validates the digest challenge
|
104
|
+
#
|
105
|
+
# @return bool
|
106
|
+
def validate
|
107
|
+
return false unless @digest_parts.any? # RUBY
|
108
|
+
|
109
|
+
a2 = @request.method + ':' + @digest_parts['uri']
|
110
|
+
|
111
|
+
if @digest_parts['qop'] == 'auth-int'
|
112
|
+
# Making sure we support this qop value
|
113
|
+
return false unless @qop & self.class::QOP_AUTHINT
|
114
|
+
|
115
|
+
# We need to add an md5 of the entire request body to the A2 part of the hash
|
116
|
+
body = @request.body_as_string
|
117
|
+
@request.body = body
|
118
|
+
|
119
|
+
a2 << ':' + ::Digest::MD5.hexdigest(body)
|
120
|
+
else
|
121
|
+
# We need to make sure we support this qop value
|
122
|
+
return false unless @qop & self.class::QOP_AUTH
|
123
|
+
end
|
124
|
+
|
125
|
+
a2 = ::Digest::MD5.hexdigest(a2)
|
126
|
+
valid_response = ::Digest::MD5.hexdigest("#{@a1}:#{@digest_parts['nonce']}:#{@digest_parts['nc']}:#{@digest_parts['cnonce']}:#{@digest_parts['qop']}:#{a2}")
|
127
|
+
|
128
|
+
@digest_parts['response'] == valid_response
|
129
|
+
end
|
130
|
+
|
131
|
+
public
|
132
|
+
|
133
|
+
# Returns an HTTP 401 header, forcing login
|
134
|
+
#
|
135
|
+
# This should be called when username and password are incorrect, or not supplied at all
|
136
|
+
#
|
137
|
+
# @return [void]
|
138
|
+
def require_login
|
139
|
+
qop = ''
|
140
|
+
case @qop
|
141
|
+
when self.class::QOP_AUTH
|
142
|
+
qop = 'auth'
|
143
|
+
when self.class::QOP_AUTHINT
|
144
|
+
qop = 'auth-int'
|
145
|
+
when self.class::QOP_AUTH | self.class::QOP_AUTHINT
|
146
|
+
qop = 'auth,auth-int'
|
147
|
+
end
|
148
|
+
|
149
|
+
@response.add_header('WWW-Authenticate', "Digest realm=\"#{@realm}\",qop=\"#{qop}\",nonce=\"#{@nonce}\",opaque=\"#{@opaque}\"")
|
150
|
+
@response.status = 401
|
151
|
+
end
|
152
|
+
|
153
|
+
# This method returns the full digest string.
|
154
|
+
#
|
155
|
+
# It should be compatibile with mod_php format and other webservers.
|
156
|
+
#
|
157
|
+
# If the header could not be found, null will be returned
|
158
|
+
#
|
159
|
+
# @return mixed
|
160
|
+
def digest
|
161
|
+
@request.header('Authorization')
|
162
|
+
end
|
163
|
+
|
164
|
+
protected
|
165
|
+
|
166
|
+
# Parses the different pieces of the digest string into an array.
|
167
|
+
#
|
168
|
+
# This method returns false if an incomplete digest was supplied
|
169
|
+
#
|
170
|
+
# @param [String] digest
|
171
|
+
# @return mixed
|
172
|
+
def parse_digest(digest)
|
173
|
+
# protect against missing data
|
174
|
+
needed_parts = { 'nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1 }
|
175
|
+
data = {}
|
176
|
+
|
177
|
+
digest.scan(/(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))/) do |m1, m2, m3|
|
178
|
+
data[m1] = m2 ? m2 : m3
|
179
|
+
needed_parts.delete m1
|
180
|
+
end
|
181
|
+
|
182
|
+
needed_parts.any? ? {} : data
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Http
|
3
|
+
# Namespace for Tilia::Http::Auth::* classes
|
4
|
+
module Auth
|
5
|
+
require 'tilia/http/auth/abstract_auth'
|
6
|
+
require 'tilia/http/auth/aws'
|
7
|
+
require 'tilia/http/auth/basic'
|
8
|
+
require 'tilia/http/auth/bearer'
|
9
|
+
require 'tilia/http/auth/digest'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|