tilia-http 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|