mongo 1.11.1 → 1.12.0.rc0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/VERSION +1 -1
- data/lib/mongo/collection.rb +8 -3
- data/lib/mongo/collection_writer.rb +1 -1
- data/lib/mongo/connection/socket/unix_socket.rb +1 -1
- data/lib/mongo/cursor.rb +8 -1
- data/lib/mongo/db.rb +61 -33
- data/lib/mongo/exception.rb +57 -0
- data/lib/mongo/functional.rb +1 -0
- data/lib/mongo/functional/authentication.rb +138 -11
- data/lib/mongo/functional/read_preference.rb +31 -22
- data/lib/mongo/functional/scram.rb +555 -0
- data/lib/mongo/functional/uri_parser.rb +107 -79
- data/lib/mongo/mongo_client.rb +19 -24
- data/lib/mongo/mongo_replica_set_client.rb +2 -1
- data/test/functional/authentication_test.rb +3 -0
- data/test/functional/client_test.rb +33 -0
- data/test/functional/collection_test.rb +29 -19
- data/test/functional/db_api_test.rb +16 -1
- data/test/functional/pool_test.rb +7 -6
- data/test/functional/uri_test.rb +111 -7
- data/test/helpers/test_unit.rb +17 -3
- data/test/replica_set/client_test.rb +31 -0
- data/test/replica_set/insert_test.rb +49 -32
- data/test/replica_set/pinning_test.rb +50 -0
- data/test/replica_set/query_test.rb +1 -1
- data/test/replica_set/replication_ack_test.rb +3 -3
- data/test/shared/authentication/basic_auth_shared.rb +14 -1
- data/test/shared/authentication/gssapi_shared.rb +13 -8
- data/test/shared/authentication/scram_shared.rb +92 -0
- data/test/tools/mongo_config.rb +18 -6
- data/test/unit/client_test.rb +40 -6
- data/test/unit/connection_test.rb +15 -5
- data/test/unit/db_test.rb +1 -1
- data/test/unit/read_pref_test.rb +291 -0
- metadata +9 -6
- metadata.gz.sig +0 -0
@@ -45,7 +45,8 @@ module Mongo
|
|
45
45
|
'mapreduce',
|
46
46
|
'replsetgetstatus',
|
47
47
|
'ismaster',
|
48
|
-
'parallelcollectionscan'
|
48
|
+
'parallelcollectionscan',
|
49
|
+
'text'
|
49
50
|
]
|
50
51
|
|
51
52
|
def self.mongos(mode, tag_sets)
|
@@ -74,7 +75,8 @@ module Mongo
|
|
74
75
|
# the server only looks at the first key in the out object
|
75
76
|
return out.respond_to?(:keys) && out.keys.first.to_s.downcase == 'inline'
|
76
77
|
elsif command == 'aggregate'
|
77
|
-
|
78
|
+
pipeline = selector['pipeline'] || selector[:pipeline]
|
79
|
+
return pipeline.none? { |op| op.key?('$out') || op.key?(:$out) }
|
78
80
|
end
|
79
81
|
SECONDARY_OK_COMMANDS.member?(command)
|
80
82
|
end
|
@@ -144,31 +146,38 @@ module Mongo
|
|
144
146
|
end
|
145
147
|
|
146
148
|
def select_secondary_pool(candidates, read_pref)
|
147
|
-
|
148
|
-
|
149
|
-
if !tag_sets.empty?
|
150
|
-
matches = []
|
151
|
-
tag_sets.detect do |tag_set|
|
152
|
-
matches = candidates.select do |candidate|
|
153
|
-
tag_set.none? { |k,v| candidate.tags[k.to_s] != v } &&
|
154
|
-
candidate.ping_time
|
155
|
-
end
|
156
|
-
!matches.empty?
|
157
|
-
end
|
158
|
-
else
|
159
|
-
matches = candidates
|
160
|
-
end
|
161
|
-
|
162
|
-
matches.empty? ? nil : select_near_pool(matches, read_pref)
|
149
|
+
matching_pools = match_tag_sets(secondary_pools, read_pref[:tags])
|
150
|
+
select_near_pools(matching_pools, read_pref).first
|
163
151
|
end
|
164
152
|
|
165
153
|
def select_near_pool(candidates, read_pref)
|
154
|
+
matching_pools = match_tag_sets(candidates, read_pref[:tags])
|
155
|
+
select_near_pools(matching_pools, read_pref).first
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def select_near_pools(candidates, read_pref)
|
161
|
+
return candidates if candidates.empty?
|
166
162
|
latency = read_pref[:latency]
|
167
|
-
nearest_pool = candidates.min_by
|
168
|
-
|
169
|
-
|
163
|
+
nearest_pool = candidates.min_by(&:ping_time)
|
164
|
+
max_latency = nearest_pool.ping_time + latency
|
165
|
+
near_pools = candidates.select { |candidate| candidate.ping_time <= max_latency }
|
166
|
+
near_pools.shuffle!
|
167
|
+
end
|
168
|
+
|
169
|
+
def match_tag_sets(candidates, tag_sets = [])
|
170
|
+
return candidates if tag_sets.empty?
|
171
|
+
matches = []
|
172
|
+
tag_sets.find do |tag_set|
|
173
|
+
matches = candidates.select do |candidate|
|
174
|
+
tag_set.none? do |k,v|
|
175
|
+
candidate.tags[k.to_s] != v
|
176
|
+
end
|
177
|
+
end
|
178
|
+
!matches.empty?
|
170
179
|
end
|
171
|
-
|
180
|
+
matches
|
172
181
|
end
|
173
182
|
end
|
174
183
|
end
|
@@ -0,0 +1,555 @@
|
|
1
|
+
# Copyright (C) 2014 MongoDB Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'base64'
|
16
|
+
require 'openssl'
|
17
|
+
require 'digest/md5'
|
18
|
+
|
19
|
+
module Mongo
|
20
|
+
module Authentication
|
21
|
+
|
22
|
+
# Defines behaviour around a single SCRAM-SHA-1 conversation between the
|
23
|
+
# client and server.
|
24
|
+
#
|
25
|
+
# @since 1.12.0
|
26
|
+
class SCRAM
|
27
|
+
|
28
|
+
# The client key string.
|
29
|
+
#
|
30
|
+
# @since 1.12.0
|
31
|
+
CLIENT_KEY = 'Client Key'.freeze
|
32
|
+
|
33
|
+
# The digest to use for encryption.
|
34
|
+
#
|
35
|
+
# @since 1.12.0
|
36
|
+
DIGEST = OpenSSL::Digest::SHA1.new.freeze
|
37
|
+
|
38
|
+
# The key for the done field in the responses.
|
39
|
+
#
|
40
|
+
# @since 1.12.0
|
41
|
+
DONE = 'done'.freeze
|
42
|
+
|
43
|
+
# The conversation id field.
|
44
|
+
#
|
45
|
+
# @since 1.12.0
|
46
|
+
ID = 'conversationId'.freeze
|
47
|
+
|
48
|
+
# The iterations key in the responses.
|
49
|
+
#
|
50
|
+
# @since 1.12.0
|
51
|
+
ITERATIONS = /i=(\d+)/.freeze
|
52
|
+
|
53
|
+
# The payload field.
|
54
|
+
#
|
55
|
+
# @since 1.12.0
|
56
|
+
PAYLOAD = 'payload'.freeze
|
57
|
+
|
58
|
+
# The rnonce key in the responses.
|
59
|
+
#
|
60
|
+
# @since 1.12.0
|
61
|
+
RNONCE = /r=([^,]*)/.freeze
|
62
|
+
|
63
|
+
# The salt key in the responses.
|
64
|
+
#
|
65
|
+
# @since 1.12.0
|
66
|
+
SALT = /s=([^,]*)/.freeze
|
67
|
+
|
68
|
+
# The server key string.
|
69
|
+
#
|
70
|
+
# @since 1.12.0
|
71
|
+
SERVER_KEY = 'Server Key'.freeze
|
72
|
+
|
73
|
+
# The server signature verifier in the response.
|
74
|
+
#
|
75
|
+
# @since 1.12.0
|
76
|
+
VERIFIER = /v=([^,]*)/.freeze
|
77
|
+
|
78
|
+
# @return [ String ] nonce The initial user nonce.
|
79
|
+
attr_reader :nonce
|
80
|
+
|
81
|
+
# @return [ BSON::OrderedHash ] reply The current reply in the conversation.
|
82
|
+
attr_reader :reply
|
83
|
+
|
84
|
+
# @return [ Hash ] auth The authentication details.
|
85
|
+
attr_reader :auth
|
86
|
+
|
87
|
+
# @return [ String ] hashed_password The user's hashed password
|
88
|
+
attr_reader :hashed_password
|
89
|
+
|
90
|
+
# Continue the SCRAM conversation. This sends the client final message
|
91
|
+
# to the server after setting the reply from the previous server
|
92
|
+
# communication.
|
93
|
+
#
|
94
|
+
# @example Continue the conversation.
|
95
|
+
# conversation.continue(reply)
|
96
|
+
#
|
97
|
+
# @param [ BSON::OrderedHash ] reply The reply of the previous
|
98
|
+
# message.
|
99
|
+
#
|
100
|
+
# @return [ BSON::OrderedHash ] The next message to send.
|
101
|
+
#
|
102
|
+
# @since 1.12.0
|
103
|
+
def continue(reply)
|
104
|
+
validate_first_message!(reply)
|
105
|
+
command = BSON::OrderedHash.new
|
106
|
+
command['saslContinue'] = 1
|
107
|
+
command[PAYLOAD] = client_final_message
|
108
|
+
command[ID] = id
|
109
|
+
command
|
110
|
+
end
|
111
|
+
|
112
|
+
# Continue the SCRAM conversation for copydb. This sends the client final message
|
113
|
+
# to the server after setting the reply from the previous server
|
114
|
+
# communication.
|
115
|
+
#
|
116
|
+
# @example Continue the conversation when copying a database.
|
117
|
+
# conversation.copy_db_continue(reply)
|
118
|
+
#
|
119
|
+
# @param [ BSON::OrderedHash ] reply The reply of the previous
|
120
|
+
# message.
|
121
|
+
#
|
122
|
+
# @return [ BSON::OrderedHash ] The next message to send.
|
123
|
+
#
|
124
|
+
# @since 1.12.0
|
125
|
+
def copy_db_continue(reply)
|
126
|
+
validate_first_message!(reply)
|
127
|
+
command = BSON::OrderedHash.new
|
128
|
+
command['copydb'] = 1
|
129
|
+
command['fromhost'] = @copy_db[:from_host]
|
130
|
+
command['fromdb'] = @copy_db[:from_db]
|
131
|
+
command['todb'] = @copy_db[:to_db]
|
132
|
+
command[PAYLOAD] = client_final_message
|
133
|
+
command[ID] = id
|
134
|
+
command
|
135
|
+
end
|
136
|
+
|
137
|
+
# Finalize the SCRAM conversation. This is meant to be iterated until
|
138
|
+
# the provided reply indicates the conversation is finished.
|
139
|
+
#
|
140
|
+
# @example Finalize the conversation.
|
141
|
+
# conversation.finalize(reply)
|
142
|
+
#
|
143
|
+
# @param [ BSON::OrderedHash ] reply The reply of the previous
|
144
|
+
# message.
|
145
|
+
#
|
146
|
+
# @return [ BSON::OrderedHash ] The next message to send.
|
147
|
+
#
|
148
|
+
# @since 1.12.0
|
149
|
+
def finalize(reply)
|
150
|
+
validate_final_message!(reply)
|
151
|
+
command = BSON::OrderedHash.new
|
152
|
+
command['saslContinue'] = 1
|
153
|
+
command[PAYLOAD] = client_empty_message
|
154
|
+
command[ID] = id
|
155
|
+
command
|
156
|
+
end
|
157
|
+
|
158
|
+
# Start the SCRAM conversation. This returns the first message that
|
159
|
+
# needs to be send to the server.
|
160
|
+
#
|
161
|
+
# @example Start the conversation.
|
162
|
+
# conversation.start
|
163
|
+
#
|
164
|
+
# @return [ BSON::OrderedHash ] The first SCRAM conversation message.
|
165
|
+
#
|
166
|
+
# @since 1.12.0
|
167
|
+
def start
|
168
|
+
command = BSON::OrderedHash.new
|
169
|
+
command['saslStart'] = 1
|
170
|
+
command['autoAuthorize'] = 1
|
171
|
+
command[PAYLOAD] = client_first_message
|
172
|
+
command['mechanism'] = 'SCRAM-SHA-1'
|
173
|
+
command
|
174
|
+
end
|
175
|
+
|
176
|
+
# Start the SCRAM conversation for copying a database.
|
177
|
+
# This returns the first message that needs to be sent to the server.
|
178
|
+
#
|
179
|
+
# @example Start the copydb conversation.
|
180
|
+
# conversation.copy_db_start
|
181
|
+
#
|
182
|
+
# @return [ BSON::OrderedHash ] The first SCRAM copy_db conversation message.
|
183
|
+
#
|
184
|
+
# @since 1.12.0
|
185
|
+
def copy_db_start
|
186
|
+
command = BSON::OrderedHash.new
|
187
|
+
command['copydbsaslstart'] = 1
|
188
|
+
command['autoAuthorize'] = 1
|
189
|
+
command['fromhost'] = @copy_db[:from_host]
|
190
|
+
command['fromdb'] = @copy_db[:from_db]
|
191
|
+
command[PAYLOAD] = client_first_message
|
192
|
+
command['mechanism'] = 'SCRAM-SHA-1'
|
193
|
+
command
|
194
|
+
end
|
195
|
+
|
196
|
+
# Get the id of the conversation.
|
197
|
+
#
|
198
|
+
# @example Get the id of the conversation.
|
199
|
+
# conversation.id
|
200
|
+
#
|
201
|
+
# @return [ Integer ] The conversation id.
|
202
|
+
#
|
203
|
+
# @since 1.12.0
|
204
|
+
def id
|
205
|
+
reply[ID]
|
206
|
+
end
|
207
|
+
|
208
|
+
# Create the new conversation.
|
209
|
+
#
|
210
|
+
# @example Create the new conversation.
|
211
|
+
# Conversation.new(auth, password)
|
212
|
+
#
|
213
|
+
# @since 1.12.0
|
214
|
+
def initialize(auth, hashed_password, opts={})
|
215
|
+
@auth = auth
|
216
|
+
@hashed_password = hashed_password
|
217
|
+
@nonce = SecureRandom.base64
|
218
|
+
@copy_db = opts[:copy_db] if opts[:copy_db]
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
# Auth message algorithm implementation.
|
224
|
+
#
|
225
|
+
# @api private
|
226
|
+
#
|
227
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
228
|
+
#
|
229
|
+
# @since 1.12.0
|
230
|
+
def auth_message
|
231
|
+
@auth_message ||= "#{first_bare},#{payload_data},#{without_proof}"
|
232
|
+
end
|
233
|
+
|
234
|
+
# Get the empty client message.
|
235
|
+
#
|
236
|
+
# @api private
|
237
|
+
#
|
238
|
+
# @since 1.12.0
|
239
|
+
def client_empty_message
|
240
|
+
BSON::Binary.new('')
|
241
|
+
end
|
242
|
+
|
243
|
+
# Get the final client message.
|
244
|
+
#
|
245
|
+
# @api private
|
246
|
+
#
|
247
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
248
|
+
#
|
249
|
+
# @since 1.12.0
|
250
|
+
def client_final_message
|
251
|
+
BSON::Binary.new("#{without_proof},p=#{client_final}")
|
252
|
+
end
|
253
|
+
|
254
|
+
# Get the client first message
|
255
|
+
#
|
256
|
+
# @api private
|
257
|
+
#
|
258
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
259
|
+
#
|
260
|
+
# @since 1.12.0
|
261
|
+
def client_first_message
|
262
|
+
BSON::Binary.new("n,,#{first_bare}")
|
263
|
+
end
|
264
|
+
|
265
|
+
# Client final implementation.
|
266
|
+
#
|
267
|
+
# @api private
|
268
|
+
#
|
269
|
+
# @see http://tools.ietf.org/html/rfc5802#section-7
|
270
|
+
#
|
271
|
+
# @since 1.12.0
|
272
|
+
def client_final
|
273
|
+
@client_final ||= client_proof(client_key, client_signature(stored_key(client_key), auth_message))
|
274
|
+
end
|
275
|
+
|
276
|
+
# Client key algorithm implementation.
|
277
|
+
#
|
278
|
+
# @api private
|
279
|
+
#
|
280
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
281
|
+
#
|
282
|
+
# @since 1.12.0
|
283
|
+
def client_key
|
284
|
+
@client_key ||= hmac(salted_password, CLIENT_KEY)
|
285
|
+
end
|
286
|
+
|
287
|
+
if Base64.respond_to?(:strict_encode64)
|
288
|
+
|
289
|
+
# Client proof algorithm implementation.
|
290
|
+
#
|
291
|
+
# @api private
|
292
|
+
#
|
293
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
294
|
+
#
|
295
|
+
# @since 1.12.0
|
296
|
+
def client_proof(key, signature)
|
297
|
+
@client_proof ||= Base64.strict_encode64(xor(key, signature))
|
298
|
+
end
|
299
|
+
else
|
300
|
+
|
301
|
+
# Client proof algorithm implementation.
|
302
|
+
#
|
303
|
+
# @api private
|
304
|
+
#
|
305
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
306
|
+
#
|
307
|
+
# @since 1.12.0
|
308
|
+
def client_proof(key, signature)
|
309
|
+
@client_proof ||= Base64.encode64(xor(key, signature)).gsub("\n",'')
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# Client signature algorithm implementation.
|
314
|
+
#
|
315
|
+
# @api private
|
316
|
+
#
|
317
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
318
|
+
#
|
319
|
+
# @since 1.12.0
|
320
|
+
def client_signature(key, message)
|
321
|
+
@client_signature ||= hmac(key, message)
|
322
|
+
end
|
323
|
+
|
324
|
+
if Base64.respond_to?(:strict_decode64)
|
325
|
+
|
326
|
+
# Get the base 64 decoded salt.
|
327
|
+
#
|
328
|
+
# @api private
|
329
|
+
#
|
330
|
+
# @since 1.12.0
|
331
|
+
def decoded_salt
|
332
|
+
@decoded_salt ||= Base64.strict_decode64(salt)
|
333
|
+
end
|
334
|
+
else
|
335
|
+
|
336
|
+
# Get the base 64 decoded salt.
|
337
|
+
#
|
338
|
+
# @api private
|
339
|
+
#
|
340
|
+
# @since 1.12.0
|
341
|
+
def decoded_salt
|
342
|
+
@decoded_salt ||= Base64.decode64(salt)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# First bare implementation.
|
347
|
+
#
|
348
|
+
# @api private
|
349
|
+
#
|
350
|
+
# @see http://tools.ietf.org/html/rfc5802#section-7
|
351
|
+
#
|
352
|
+
# @since 1.12.0
|
353
|
+
def first_bare
|
354
|
+
@first_bare ||= "n=#{auth[:username].gsub('=','=3D').gsub(',','=2C')},r=#{nonce}"
|
355
|
+
end
|
356
|
+
|
357
|
+
# H algorithm implementation.
|
358
|
+
#
|
359
|
+
# @api private
|
360
|
+
#
|
361
|
+
# @see http://tools.ietf.org/html/rfc5802#section-2.2
|
362
|
+
#
|
363
|
+
# @since 1.12.0
|
364
|
+
def h(string)
|
365
|
+
DIGEST.digest(string)
|
366
|
+
end
|
367
|
+
|
368
|
+
if defined?(OpenSSL::PKCS5)
|
369
|
+
|
370
|
+
# HI algorithm implementation.
|
371
|
+
#
|
372
|
+
# @api private
|
373
|
+
#
|
374
|
+
# @see http://tools.ietf.org/html/rfc5802#section-2.2
|
375
|
+
#
|
376
|
+
# @since 1.12.0
|
377
|
+
def hi(data)
|
378
|
+
OpenSSL::PKCS5.pbkdf2_hmac_sha1(data, decoded_salt, iterations, DIGEST.size)
|
379
|
+
end
|
380
|
+
else
|
381
|
+
|
382
|
+
# HI algorithm implementation.
|
383
|
+
#
|
384
|
+
# @api private
|
385
|
+
#
|
386
|
+
# @see http://tools.ietf.org/html/rfc5802#section-2.2
|
387
|
+
#
|
388
|
+
# @since 1.12.0
|
389
|
+
def hi(data)
|
390
|
+
u = hmac(data, decoded_salt + [1].pack("N"))
|
391
|
+
v = u
|
392
|
+
2.upto(iterations) do |i|
|
393
|
+
u = hmac(data, u)
|
394
|
+
v = xor(v, u)
|
395
|
+
end
|
396
|
+
v
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# HMAC algorithm implementation.
|
401
|
+
#
|
402
|
+
# @api private
|
403
|
+
#
|
404
|
+
# @see http://tools.ietf.org/html/rfc5802#section-2.2
|
405
|
+
#
|
406
|
+
# @since 1.12.0
|
407
|
+
def hmac(data, key)
|
408
|
+
OpenSSL::HMAC.digest(DIGEST, data, key)
|
409
|
+
end
|
410
|
+
|
411
|
+
# Get the iterations from the server response.
|
412
|
+
#
|
413
|
+
# @api private
|
414
|
+
#
|
415
|
+
# @since 1.12.0
|
416
|
+
def iterations
|
417
|
+
@iterations ||= payload_data.match(ITERATIONS)[1].to_i
|
418
|
+
end
|
419
|
+
|
420
|
+
# Get the data from the returned payload.
|
421
|
+
#
|
422
|
+
# @api private
|
423
|
+
#
|
424
|
+
# @since 1.12.0
|
425
|
+
def payload_data
|
426
|
+
reply[PAYLOAD].to_s
|
427
|
+
end
|
428
|
+
|
429
|
+
# Get the server nonce from the payload.
|
430
|
+
#
|
431
|
+
# @api private
|
432
|
+
#
|
433
|
+
# @since 1.12.0
|
434
|
+
def rnonce
|
435
|
+
@rnonce ||= payload_data.match(RNONCE)[1]
|
436
|
+
end
|
437
|
+
|
438
|
+
# Gets the salt from the server response.
|
439
|
+
#
|
440
|
+
# @api private
|
441
|
+
#
|
442
|
+
# @since 1.12.0
|
443
|
+
def salt
|
444
|
+
@salt ||= payload_data.match(SALT)[1]
|
445
|
+
end
|
446
|
+
|
447
|
+
# Salted password algorithm implementation.
|
448
|
+
#
|
449
|
+
# @api private
|
450
|
+
#
|
451
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
452
|
+
#
|
453
|
+
# @since 1.12.0
|
454
|
+
def salted_password
|
455
|
+
@salted_password ||= hi(hashed_password)
|
456
|
+
end
|
457
|
+
|
458
|
+
# Server key algorithm implementation.
|
459
|
+
#
|
460
|
+
# @api private
|
461
|
+
#
|
462
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
463
|
+
#
|
464
|
+
# @since 1.12.0
|
465
|
+
def server_key
|
466
|
+
@server_key ||= hmac(salted_password, SERVER_KEY)
|
467
|
+
end
|
468
|
+
|
469
|
+
if Base64.respond_to?(:strict_encode64)
|
470
|
+
|
471
|
+
# Server signature algorithm implementation.
|
472
|
+
#
|
473
|
+
# @api private
|
474
|
+
#
|
475
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
476
|
+
#
|
477
|
+
# @since 1.12.0
|
478
|
+
def server_signature
|
479
|
+
@server_signature ||= Base64.strict_encode64(hmac(server_key, auth_message))
|
480
|
+
end
|
481
|
+
else
|
482
|
+
|
483
|
+
# Server signature algorithm implementation.
|
484
|
+
#
|
485
|
+
# @api private
|
486
|
+
#
|
487
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
488
|
+
#
|
489
|
+
# @since 1.12.0
|
490
|
+
def server_signature
|
491
|
+
@server_signature ||= Base64.encode64(hmac(server_key, auth_message)).gsub("\n",'')
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# Stored key algorithm implementation.
|
496
|
+
#
|
497
|
+
# @api private
|
498
|
+
#
|
499
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
500
|
+
#
|
501
|
+
# @since 1.12.0
|
502
|
+
def stored_key(key)
|
503
|
+
h(key)
|
504
|
+
end
|
505
|
+
|
506
|
+
# Get the verifier token from the server response.
|
507
|
+
#
|
508
|
+
# @api private
|
509
|
+
#
|
510
|
+
# @since 1.12.0
|
511
|
+
def verifier
|
512
|
+
@verifier ||= payload_data.match(VERIFIER)[1]
|
513
|
+
end
|
514
|
+
|
515
|
+
# Get the without proof message.
|
516
|
+
#
|
517
|
+
# @api private
|
518
|
+
#
|
519
|
+
# @see http://tools.ietf.org/html/rfc5802#section-7
|
520
|
+
#
|
521
|
+
# @since 1.12.0
|
522
|
+
def without_proof
|
523
|
+
@without_proof ||= "c=biws,r=#{rnonce}"
|
524
|
+
end
|
525
|
+
|
526
|
+
# XOR operation for two strings.
|
527
|
+
#
|
528
|
+
# @api private
|
529
|
+
#
|
530
|
+
# @since 1.12.0
|
531
|
+
def xor(first, second)
|
532
|
+
first.bytes.zip(second.bytes).map{ |(a,b)| (a ^ b).chr }.join('')
|
533
|
+
end
|
534
|
+
|
535
|
+
def validate_final_message!(reply)
|
536
|
+
validate!(reply)
|
537
|
+
unless verifier == server_signature
|
538
|
+
raise InvalidSignature.new(verifier, server_signature)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
def validate_first_message!(reply)
|
543
|
+
validate!(reply)
|
544
|
+
raise InvalidNonce.new(nonce, rnonce) unless rnonce.start_with?(nonce)
|
545
|
+
end
|
546
|
+
|
547
|
+
def validate!(reply)
|
548
|
+
unless Support.ok?(reply)
|
549
|
+
raise AuthenticationError, "Could not authorize user #{auth[:username]} on database #{auth[:db_name]}."
|
550
|
+
end
|
551
|
+
@reply = reply
|
552
|
+
end
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|