em-mongo 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG +9 -0
- data/README.rdoc +17 -2
- data/VERSION +1 -1
- data/em-mongo.gemspec +6 -4
- data/examples/legacy.rb +8 -1
- data/lib/em-mongo.rb +4 -6
- data/lib/em-mongo/auth/Authentication.rb +37 -0
- data/lib/em-mongo/auth/mongodb_cr.rb +52 -0
- data/lib/em-mongo/auth/scram.rb +237 -0
- data/lib/em-mongo/collection.rb +54 -0
- data/lib/em-mongo/connection.rb +0 -1
- data/lib/em-mongo/database.rb +17 -29
- data/lib/em-mongo/support.rb +2 -12
- data/lib/em-mongo/version.rb +11 -0
- data/spec/integration/collection_spec.rb +13 -0
- data/spec/integration/database_spec.rb +12 -0
- metadata +20 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f0b2c50ede300816de67e11528b73a7d1e337f1
|
4
|
+
data.tar.gz: fbc4c31accd66a72abf2aa1e70397cf4c66fcd6b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cca17b4be1e91a7d1a787054a49e3205a237eddebd44387134e4085c20337c87f1375e3a6da5bab1b98ab971f8dd3ac56bda6675a147ef0097a5d6150c4cc0e5
|
7
|
+
data.tar.gz: c0f8f8f69281f7046e5a9b91b5c819895224d4fa1edf0e6db620ab901c5ef1a7a0d1ef23f62c51be2706d5ecc7bd79c4433d1c1cc15f179e4b9e5725a96370bd
|
data/.gitignore
CHANGED
data/CHANGELOG
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
- 0.6.0
|
2
|
+
|
3
|
+
* add SCRAM-SHA-1 authentication mechanism
|
4
|
+
* add aggregation pipeline
|
5
|
+
* allow passing options to create_collection
|
6
|
+
|
7
|
+
* fix usage of em-mongo git repos with bundler
|
8
|
+
* update dependencies to bson <= 2.0, eventmachine >=0.12.10, <= 2.0
|
9
|
+
|
1
10
|
- 0.5.1
|
2
11
|
|
3
12
|
* fixed bson dependency (do not use 2.x)
|
data/README.rdoc
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
|
2
|
-
Em-mongo
|
3
|
-
or want to take over the full project, please let me know!
|
2
|
+
Em-mongo needs collaborators to help maintaining this project. If you are interested, please let me know!
|
4
3
|
|
5
4
|
= EM-Mongo
|
6
5
|
|
@@ -122,6 +121,22 @@ In addition to calling your errback if the write fails, you can provide the usua
|
|
122
121
|
|
123
122
|
safe_insert( {:a=>"v"}, :last_error_params => { :fsync => true, :w => 5 } )
|
124
123
|
|
124
|
+
|
125
|
+
== Authentication
|
126
|
+
At the moment the mechhanisms SCRAM-SHA-1(:scram_sha1) and MONGODB-CR(:mongodb_cr) are supported.
|
127
|
+
It works as follows:
|
128
|
+
|
129
|
+
# establish connection and get authentication db
|
130
|
+
connection = EM::Mongo::Connection.new(db_server, port)
|
131
|
+
auth_db = connection.db(db_name)
|
132
|
+
|
133
|
+
# authentication itself
|
134
|
+
resp = auth_db.authenticate(user,password, :scram_sha1)
|
135
|
+
resp.callback do |success|
|
136
|
+
do_authenticated #auth successful
|
137
|
+
end
|
138
|
+
resp.errback {|err| auth_failure err} # authentication failed
|
139
|
+
|
125
140
|
== Documentation
|
126
141
|
|
127
142
|
em-mongo now has some YARD docs. These are mostly ported directly from the mongo-ruby-driver. While they have been updated to reflect em-mongo's async API, there are probably a few errors left over in the translation. Please file an issue or submit a pull request if you notice any inaccuracies.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
data/em-mongo.gemspec
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'em-mongo/version.rb' #for version
|
2
4
|
|
3
5
|
Gem::Specification.new do |s|
|
4
6
|
s.name = 'em-mongo'
|
5
|
-
s.version =
|
7
|
+
s.version = EventMachine::Mongo::VERSION
|
6
8
|
|
7
9
|
s.authors = ['bcg', 'PlasticLizard']
|
8
10
|
s.email = 'brenden.grace@gmail.com'
|
@@ -23,6 +25,6 @@ Gem::Specification.new do |s|
|
|
23
25
|
|
24
26
|
s.summary = 'An EventMachine driver for MongoDB.'
|
25
27
|
|
26
|
-
s.add_dependency 'eventmachine', ['>=
|
27
|
-
s.add_dependency
|
28
|
+
s.add_dependency 'eventmachine', ['>=0.12.10', '< 2.0']
|
29
|
+
s.add_dependency 'bson', ['>=1.9.2' , '< 2.0']
|
28
30
|
end
|
data/examples/legacy.rb
CHANGED
@@ -5,9 +5,16 @@ require 'em-mongo/prev.rb'
|
|
5
5
|
require 'eventmachine'
|
6
6
|
|
7
7
|
EM.run do
|
8
|
-
conn = EM::Mongo::Connection.new('localhost')
|
8
|
+
conn = EM::Mongo::Connection.new('localhost',27017)
|
9
9
|
db = conn.db('my_database')
|
10
10
|
collection = db.collection('my_collection')
|
11
|
+
|
12
|
+
resp = conn.db('admin').authenticate('test','test')
|
13
|
+
|
14
|
+
resp.callback{|response| puts "successfully authenticated #{response}"}
|
15
|
+
resp.errback {|response| puts "error on authentication #{response}"; return}
|
16
|
+
|
17
|
+
|
11
18
|
EM.next_tick do
|
12
19
|
|
13
20
|
(1..10).each do |i|
|
data/lib/em-mongo.rb
CHANGED
@@ -5,18 +5,14 @@ rescue LoadError
|
|
5
5
|
require "bson"
|
6
6
|
end
|
7
7
|
|
8
|
-
module EM::Mongo
|
9
8
|
|
10
|
-
|
11
|
-
STRING = File.read(File.dirname(__FILE__) + '/../VERSION')
|
12
|
-
MAJOR, MINOR, TINY = STRING.split('.')
|
13
|
-
end
|
9
|
+
module EM::Mongo
|
14
10
|
|
15
11
|
NAME = 'em-mongo'
|
16
12
|
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
17
13
|
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
18
14
|
end
|
19
|
-
|
15
|
+
require File.join(EM::Mongo::LIBPATH, "em-mongo/version")
|
20
16
|
require File.join(EM::Mongo::LIBPATH, "em-mongo/conversions")
|
21
17
|
require File.join(EM::Mongo::LIBPATH, "em-mongo/support")
|
22
18
|
require File.join(EM::Mongo::LIBPATH, "em-mongo/database")
|
@@ -27,5 +23,7 @@ require File.join(EM::Mongo::LIBPATH, "em-mongo/cursor")
|
|
27
23
|
require File.join(EM::Mongo::LIBPATH, "em-mongo/request_response")
|
28
24
|
require File.join(EM::Mongo::LIBPATH, "em-mongo/server_response")
|
29
25
|
require File.join(EM::Mongo::LIBPATH, "em-mongo/core_ext")
|
26
|
+
require File.join(EM::Mongo::LIBPATH, "em-mongo/auth/Authentication.rb")
|
27
|
+
|
30
28
|
|
31
29
|
EMMongo = EM::Mongo
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
# interface for all possible authentications
|
4
|
+
module EM::Mongo
|
5
|
+
class Authentication
|
6
|
+
include EM::Deferrable
|
7
|
+
|
8
|
+
SYSTEM_COMMAND_COLLECTION = '$cmd'
|
9
|
+
|
10
|
+
# supported AuthMethods (TODO make instantiation (in database.authenticate) dynamic)
|
11
|
+
module AuthMethod
|
12
|
+
SCRAM_SHA1 = :scram_sha1
|
13
|
+
MONGODB_CR = :mongodb_cr
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(database)
|
17
|
+
@db = database
|
18
|
+
end
|
19
|
+
|
20
|
+
# Authenticate with the given username and password. Note that mongod
|
21
|
+
# must be started with the --auth option for authentication to be enabled.
|
22
|
+
#
|
23
|
+
# @param [String] username
|
24
|
+
# @param [String] password
|
25
|
+
#
|
26
|
+
# @return [EM::Mongo::RequestResponse] Calls back with +true+ or +false+, indicating success or failure
|
27
|
+
#
|
28
|
+
# @raise [AuthenticationError]
|
29
|
+
#
|
30
|
+
# @core authenticate authenticate-instance_method
|
31
|
+
def authenticate(username, password)
|
32
|
+
r=DefaultDeferrable.new #stub implementation
|
33
|
+
r.fail "not implemented, use a subclass instead"
|
34
|
+
return r
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require_relative '../support.rb'
|
3
|
+
|
4
|
+
module EM::Mongo
|
5
|
+
class MONGODB_CR < Authentication
|
6
|
+
|
7
|
+
MECHANISM = 'MONGODB-CR'.freeze
|
8
|
+
|
9
|
+
def authenticate(username, password)
|
10
|
+
response = RequestResponse.new
|
11
|
+
|
12
|
+
auth_resp = @db.collection(SYSTEM_COMMAND_COLLECTION).first({'getnonce' => 1})
|
13
|
+
auth_resp.callback do |res|
|
14
|
+
if not res or not res['nonce']
|
15
|
+
if res.nil? then response.fail "connection failure"
|
16
|
+
else response.fail "invalid first server response: " + res.to_s
|
17
|
+
end
|
18
|
+
else
|
19
|
+
auth = BSON::OrderedHash.new
|
20
|
+
auth['authenticate'] = 1
|
21
|
+
auth['user'] = username
|
22
|
+
auth['nonce'] = res['nonce']
|
23
|
+
auth['key'] = auth_key(username, password, res['nonce'])
|
24
|
+
|
25
|
+
auth_resp2 = @db.collection(SYSTEM_COMMAND_COLLECTION).first(auth)
|
26
|
+
auth_resp2.callback do |res|
|
27
|
+
if Support.ok?(res)
|
28
|
+
response.succeed true
|
29
|
+
else
|
30
|
+
response.fail res
|
31
|
+
end
|
32
|
+
end
|
33
|
+
auth_resp2.errback { |err| response.fail err }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
auth_resp.errback { |err| response.fail err }
|
37
|
+
response
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generate an MD5 for authentication.
|
41
|
+
#
|
42
|
+
# @param [String] username
|
43
|
+
# @param [String] password
|
44
|
+
# @param [String] nonce
|
45
|
+
#
|
46
|
+
# @return [String] a key for db authentication.
|
47
|
+
def auth_key(username, password, nonce)
|
48
|
+
OpenSSL::Digest::MD5.hexdigest("#{nonce}#{username}#{Support.hash_password(username, password)}")
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'bson'
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
require_relative '../support.rb'
|
6
|
+
|
7
|
+
module EM::Mongo
|
8
|
+
|
9
|
+
# an RFC 5802 compilant SCRAM(-SHA-1) implementation
|
10
|
+
# for MongoDB-Authentication
|
11
|
+
#
|
12
|
+
# so everything is encapsulated, but the main part (PAYLOAD of messages) is RFC5802 compilant
|
13
|
+
class SCRAM < Authentication
|
14
|
+
|
15
|
+
MECHANISM = 'SCRAM-SHA-1'.freeze
|
16
|
+
|
17
|
+
DIGEST = OpenSSL::Digest::SHA1.new.freeze
|
18
|
+
|
19
|
+
CLIENT_FIRST_MESSAGE = { saslStart: 1, autoAuthorize: 1 }.freeze
|
20
|
+
CLIENT_FINAL_MESSAGE = CLIENT_EMPTY_MESSAGE = { saslContinue: 1 }.freeze
|
21
|
+
|
22
|
+
|
23
|
+
CLIENT_KEY = 'Client Key'.freeze
|
24
|
+
SERVER_KEY = 'Server Key'.freeze
|
25
|
+
|
26
|
+
RNONCE = /r=([^,]*)/.freeze
|
27
|
+
SALT = /s=([^,]*)/.freeze
|
28
|
+
ITERATIONS = /i=(\d+)/.freeze
|
29
|
+
VERIFIER = /v=([^,]*)/.freeze
|
30
|
+
PAYLOAD = 'payload'.freeze
|
31
|
+
|
32
|
+
# @param [String] username
|
33
|
+
# @param [String] password
|
34
|
+
#
|
35
|
+
# @return [EM::Mongo::RequestResponse] Calls back with +true+ or +false+, indicating success or failure
|
36
|
+
#
|
37
|
+
# @raise [AuthenticationError]
|
38
|
+
#
|
39
|
+
# @core authenticate authenticate-instance_method
|
40
|
+
def authenticate(username, password)
|
41
|
+
response = RequestResponse.new
|
42
|
+
|
43
|
+
#TODO look for fail-fast-ness (strange word!?)
|
44
|
+
#TODO Flatten Hierarchies
|
45
|
+
@username = username
|
46
|
+
@plain_password = password
|
47
|
+
|
48
|
+
gs2_header = 'n,,'
|
49
|
+
client_first_bare = "n=#{@username},r=#{client_nonce}"
|
50
|
+
|
51
|
+
client_first = BSON::Binary.new(gs2_header+client_first_bare) # client_first msg
|
52
|
+
client_first_msg = CLIENT_FIRST_MESSAGE.merge({PAYLOAD=>client_first, mechanism:MECHANISM})
|
53
|
+
|
54
|
+
client_first_resp = @db.collection(EM::Mongo::Database::SYSTEM_COMMAND_COLLECTION).first(client_first_msg) #TODO extract and make easier to understand (e.g. command(first_msg) or sthg like that)
|
55
|
+
|
56
|
+
#server_first_resp #for flattening
|
57
|
+
|
58
|
+
client_first_resp.callback do |res_first|
|
59
|
+
if not is_server_response_valid? res_first
|
60
|
+
response.fail "first server response not valid: " + res_first.to_s
|
61
|
+
else
|
62
|
+
# take the salt & iterations and do the pw-derivation
|
63
|
+
server_first = res_first[PAYLOAD].to_s
|
64
|
+
|
65
|
+
@conversation_id=conv_id = res_first['conversationId']
|
66
|
+
|
67
|
+
combined_nonce = server_first.match(RNONCE)[1] #r= ...
|
68
|
+
salt = server_first.match( SALT )[1] #s=... (from server_first)
|
69
|
+
iterations = server_first.match(ITERATIONS)[1].to_i #i=... ..
|
70
|
+
|
71
|
+
if not combined_nonce.start_with?(client_nonce) # combined_nonce should be client_nonce+server_nonce
|
72
|
+
response.fail "nonce doesn't start with client_nonce: " + res_first.to_s
|
73
|
+
else
|
74
|
+
client_final_wo_proof= "c=#{Base64.strict_encode64(gs2_header)},r=#{combined_nonce}" #c='biws'
|
75
|
+
auth_message = client_first_bare + ',' + server_first + ',' + client_final_wo_proof
|
76
|
+
|
77
|
+
# proof = clientKey XOR clientSig ## needs to be sent back
|
78
|
+
#
|
79
|
+
# ClientSign = HMAC(StoredKey, AuthMessage)
|
80
|
+
# StoredKey = H(ClientKey) ## lt. RFC5802 (needs to be verified against ruby-mongo driver impl)
|
81
|
+
# AuthMessage = client_first_bare + ','+server_first+','+client_final_wo_proof
|
82
|
+
|
83
|
+
@salt = salt
|
84
|
+
@iterations = iterations
|
85
|
+
#client_key = client_key()
|
86
|
+
|
87
|
+
@auth_message = auth_message
|
88
|
+
#client_signature = client_signature()
|
89
|
+
|
90
|
+
proof = Base64.strict_encode64(xor(client_key, client_signature))
|
91
|
+
client_final = BSON::Binary.new ( client_final_wo_proof + ",p=#{proof}")
|
92
|
+
client_final_msg = CLIENT_FINAL_MESSAGE.merge({PAYLOAD => client_final, conversationId: conv_id})
|
93
|
+
|
94
|
+
client_final_resp = @db.collection(SYSTEM_COMMAND_COLLECTION).first(client_final_msg)
|
95
|
+
client_final_resp.callback do |res_final|
|
96
|
+
if not is_server_response_valid? res_final
|
97
|
+
response.fail "Final Server Response not valid " + res_final.to_s
|
98
|
+
else
|
99
|
+
server_final = res_final[PAYLOAD].to_s # in RFC this equals server_final
|
100
|
+
verifier = server_final.match(VERIFIER)[1] #r= ...
|
101
|
+
if verifier and verifier_valid? verifier
|
102
|
+
handle_server_end(response,conv_id) # will set the response
|
103
|
+
else
|
104
|
+
response.fail "verifier #{verifier.nil? ? 'not present':'invalid'} #{res_final}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
client_final_resp.errback { |err| response.fail err }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
client_first_resp.errback {
|
113
|
+
|err| response.fail err }
|
114
|
+
return response
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# MongoDB handles the end of authentication different than in RFC 5802
|
119
|
+
# it needs at least an additional empty response (this needs to be iterated until res[done]=true
|
120
|
+
# (at least it is done so in the official mongo-ruby-drive (at least it is done so in the official mongo-ruby-driver))
|
121
|
+
# -> recursion (is technically more loop than recursion but here it's one)
|
122
|
+
#
|
123
|
+
# @param response [EM::Mongo::ResponseRequest] to fail or succeed after completion
|
124
|
+
# @param conv_id ConversationId to send to the server on each iteration
|
125
|
+
def handle_server_end(response,conv_id) # will set the response
|
126
|
+
client_end = BSON::Binary.new('')
|
127
|
+
client_end_msg = CLIENT_EMPTY_MESSAGE.merge(PAYLOAD=>client_end, conversationId:conv_id)
|
128
|
+
server_end_resp = @db.collection(SYSTEM_COMMAND_COLLECTION).first(client_end_msg)
|
129
|
+
|
130
|
+
server_end_resp.errback{|err| response.fail err}
|
131
|
+
|
132
|
+
server_end_resp.callback do |res|
|
133
|
+
if not is_server_response_valid? res
|
134
|
+
response.fail "got invalid response on handling server_end: #{res.nil? ? 'nil' : res}"
|
135
|
+
else
|
136
|
+
if res['done'] == true || res['done'] == 'true'
|
137
|
+
response.succeed true
|
138
|
+
else
|
139
|
+
handle_server_end(response,conv_id) # try it again
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# to be valid the response has to
|
146
|
+
# * be not nil
|
147
|
+
# * contain at least ['done'], ['ok'], ['payload'], ['conversationId']
|
148
|
+
# * ['ok'].to_i has to be 1
|
149
|
+
# * ['conversationId'] has to match the first sent one
|
150
|
+
# @param [BSON::OrderedHash] response the response got from server
|
151
|
+
def is_server_response_valid?(response)
|
152
|
+
if response.nil? then return false; end
|
153
|
+
if response['done'].nil? or
|
154
|
+
response['ok'].nil? or
|
155
|
+
response['payload'].nil? or
|
156
|
+
response['conversationId'].nil? then
|
157
|
+
return false;
|
158
|
+
end
|
159
|
+
|
160
|
+
if not Support.ok? response then return false; end
|
161
|
+
if not @conversation_id.nil? and response['conversationId'] != @conversation_id
|
162
|
+
return false;
|
163
|
+
end
|
164
|
+
|
165
|
+
true
|
166
|
+
end
|
167
|
+
|
168
|
+
## verify the verifier (v=...)
|
169
|
+
def verifier_valid?(verifier)
|
170
|
+
verifier == server_signature
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
### Building blocks
|
175
|
+
# @see http://tools.ietf.org/html/rfc5802#section-2.2
|
176
|
+
|
177
|
+
def hi(password, salt, iterations)
|
178
|
+
OpenSSL::PKCS5.pbkdf2_hmac_sha1(
|
179
|
+
password,
|
180
|
+
Base64.strict_decode64(salt),
|
181
|
+
iterations,
|
182
|
+
DIGEST.size
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
def hmac(data,key)
|
187
|
+
OpenSSL::HMAC.digest(DIGEST, data, key)
|
188
|
+
end
|
189
|
+
|
190
|
+
# xor for strings
|
191
|
+
def xor(first, second)
|
192
|
+
first.bytes
|
193
|
+
.zip(second.bytes)
|
194
|
+
.map{|(x,y)| (x ^ y).chr}
|
195
|
+
.join('')
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
def client_nonce
|
200
|
+
@client_nonce ||= SecureRandom.base64
|
201
|
+
end
|
202
|
+
|
203
|
+
# needs @username, @plain_password defined
|
204
|
+
def hashed_password
|
205
|
+
@hashed_password ||= Support.hash_password(@username, @plain_password).encode("UTF-8")
|
206
|
+
end
|
207
|
+
|
208
|
+
#needs @username, @plain_password, @salt, @iterations defined
|
209
|
+
def salted_password
|
210
|
+
@salted_password ||= hi(hashed_password, @salt, @iterations)
|
211
|
+
end
|
212
|
+
|
213
|
+
# @see http://tools.ietf.org/html/rfc5802#section-3
|
214
|
+
def client_key
|
215
|
+
@client_key ||= hmac(salted_password,CLIENT_KEY)
|
216
|
+
end
|
217
|
+
# server_key = hmac(salted_password,"Server Key")
|
218
|
+
def server_key
|
219
|
+
@server_key ||= hmac(salted_password,SERVER_KEY)
|
220
|
+
end
|
221
|
+
|
222
|
+
#needs @username, @plain_password, @salt, @iterations, @auth_message defined
|
223
|
+
def client_signature
|
224
|
+
@client_signature ||= hmac(DIGEST.digest(client_key), @auth_message)
|
225
|
+
end
|
226
|
+
|
227
|
+
# server_signature = B64(hmac(server_key, auth_message)
|
228
|
+
def server_signature
|
229
|
+
@server_signature ||= Base64.strict_encode64(hmac(server_key, @auth_message))
|
230
|
+
end
|
231
|
+
|
232
|
+
class FirstMessage
|
233
|
+
include EM::Deferrable
|
234
|
+
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
data/lib/em-mongo/collection.rb
CHANGED
@@ -401,6 +401,59 @@ module EM::Mongo
|
|
401
401
|
response
|
402
402
|
end
|
403
403
|
|
404
|
+
# Perform an aggregation using the aggregation framework on the current collection.
|
405
|
+
# @note Aggregate requires server version >= 2.1.1
|
406
|
+
# @note Field References: Within an expression, field names must be quoted and prefixed by a dollar sign ($).
|
407
|
+
#
|
408
|
+
# @example Define the pipeline as an array of operator hashes:
|
409
|
+
# coll.aggregate([ {"$project" => {"last_name" => 1, "first_name" => 1 }}, {"$match" => {"last_name" => "Jones"}} ])
|
410
|
+
#
|
411
|
+
# @param [Array] pipeline Should be a single array of pipeline operator hashes.
|
412
|
+
#
|
413
|
+
# '$project' Reshapes a document stream by including fields, excluding fields, inserting computed fields,
|
414
|
+
# renaming fields,or creating/populating fields that hold sub-documents.
|
415
|
+
#
|
416
|
+
# '$match' Query-like interface for filtering documents out of the aggregation pipeline.
|
417
|
+
#
|
418
|
+
# '$limit' Restricts the number of documents that pass through the pipeline.
|
419
|
+
#
|
420
|
+
# '$skip' Skips over the specified number of documents and passes the rest along the pipeline.
|
421
|
+
#
|
422
|
+
# '$unwind' Peels off elements of an array individually, returning one document for each member.
|
423
|
+
#
|
424
|
+
# '$group' Groups documents for calculating aggregate values.
|
425
|
+
#
|
426
|
+
# '$sort' Sorts all input documents and returns them to the pipeline in sorted order.
|
427
|
+
#
|
428
|
+
# @option opts [:primary, :secondary] :read Read preference indicating which server to perform this query
|
429
|
+
# on. See Collection#find for more details.
|
430
|
+
# @option opts [String] :comment (nil) a comment to include in profiling logs
|
431
|
+
#
|
432
|
+
# @return [Array] An Array with the aggregate command's results.
|
433
|
+
#
|
434
|
+
# @raise MongoArgumentError if operators either aren't supplied or aren't in the correct format.
|
435
|
+
# @raise MongoOperationFailure if the aggregate command fails.
|
436
|
+
#
|
437
|
+
def aggregate(pipeline=nil, opts={})
|
438
|
+
response = RequestResponse.new
|
439
|
+
raise MongoArgumentError, "pipeline must be an array of operators" unless pipeline.class == Array
|
440
|
+
raise MongoArgumentError, "pipeline operators must be hashes" unless pipeline.all? { |op| op.class == Hash }
|
441
|
+
|
442
|
+
hash = BSON::OrderedHash.new
|
443
|
+
hash['aggregate'] = self.name
|
444
|
+
hash['pipeline'] = pipeline
|
445
|
+
|
446
|
+
cmd_resp = db.command(hash)
|
447
|
+
cmd_resp.callback do |resp|
|
448
|
+
response.succeed resp["result"]
|
449
|
+
end
|
450
|
+
cmd_resp.errback do |err|
|
451
|
+
response.fail err
|
452
|
+
end
|
453
|
+
|
454
|
+
response
|
455
|
+
end
|
456
|
+
|
404
457
|
# Perform a map-reduce operation on the current collection.
|
405
458
|
#
|
406
459
|
# @param [String, BSON::Code] map a map function, written in JavaScript.
|
@@ -809,3 +862,4 @@ module EM::Mongo
|
|
809
862
|
|
810
863
|
end
|
811
864
|
end
|
865
|
+
|
data/lib/em-mongo/connection.rb
CHANGED
data/lib/em-mongo/database.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
require_relative 'auth/Authentication.rb'
|
2
|
+
require_relative 'auth/scram.rb'
|
3
|
+
require_relative 'auth/mongodb_cr.rb'
|
4
|
+
|
1
5
|
module EM::Mongo
|
2
6
|
class Database
|
3
7
|
|
@@ -83,7 +87,7 @@ module EM::Mongo
|
|
83
87
|
# a cursor which can be iterated over. For each collection, a hash
|
84
88
|
# will be yielded containing a 'name' string and, optionally, an 'options' hash.
|
85
89
|
#
|
86
|
-
# @param [String] coll_name return info for the
|
90
|
+
# @param [String] coll_name return info for the specified collection only.
|
87
91
|
#
|
88
92
|
# @return [EM::Mongo::Cursor]
|
89
93
|
def collections_info(coll_name=nil)
|
@@ -114,7 +118,7 @@ module EM::Mongo
|
|
114
118
|
# already exists or collection creation fails on the server.
|
115
119
|
#
|
116
120
|
# @return [EM::Mongo::RequestResponse] Calls back with the new collection
|
117
|
-
def create_collection(name)
|
121
|
+
def create_collection(name, opts = {})
|
118
122
|
response = RequestResponse.new
|
119
123
|
names_resp = collection_names
|
120
124
|
names_resp.callback do |names|
|
@@ -125,6 +129,7 @@ module EM::Mongo
|
|
125
129
|
# Create a new collection.
|
126
130
|
oh = BSON::OrderedHash.new
|
127
131
|
oh[:create] = name
|
132
|
+
oh.merge! opts
|
128
133
|
cmd_resp = command(oh)
|
129
134
|
cmd_resp.callback do |doc|
|
130
135
|
if EM::Mongo::Support.ok?(doc)
|
@@ -302,7 +307,7 @@ module EM::Mongo
|
|
302
307
|
check_response = opts.fetch(:check_response, true)
|
303
308
|
raise MongoArgumentError, "command must be given a selector" unless selector.is_a?(Hash) && !selector.empty?
|
304
309
|
|
305
|
-
if selector.
|
310
|
+
if selector.size > 1 && RUBY_VERSION < '1.9' && selector.class != BSON::OrderedHash
|
306
311
|
raise MongoArgumentError, "DB#command requires an OrderedHash when hash contains multiple keys"
|
307
312
|
end
|
308
313
|
|
@@ -331,38 +336,20 @@ module EM::Mongo
|
|
331
336
|
#
|
332
337
|
# @param [String] username
|
333
338
|
# @param [String] password
|
339
|
+
# @param [Authentication::AuthMethod] auth_method, defaults to MONGODB_CR for downward compatibility
|
334
340
|
#
|
335
341
|
# @return [EM::Mongo::RequestResponse] Calls back with +true+ or +false+, indicating success or failure
|
336
342
|
#
|
337
343
|
# @raise [AuthenticationError]
|
338
344
|
#
|
339
345
|
# @core authenticate authenticate-instance_method
|
340
|
-
def authenticate(username, password)
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
auth = BSON::OrderedHash.new
|
348
|
-
auth['authenticate'] = 1
|
349
|
-
auth['user'] = username
|
350
|
-
auth['nonce'] = res['nonce']
|
351
|
-
auth['key'] = EM::Mongo::Support.auth_key(username, password, res['nonce'])
|
352
|
-
|
353
|
-
auth_resp2 = self.collection(SYSTEM_COMMAND_COLLECTION).first(auth)
|
354
|
-
auth_resp2.callback do |res|
|
355
|
-
if EM::Mongo::Support.ok?(res)
|
356
|
-
response.succeed true
|
357
|
-
else
|
358
|
-
response.fail res
|
359
|
-
end
|
360
|
-
end
|
361
|
-
auth_resp2.errback { |err| response.fail err }
|
362
|
-
end
|
363
|
-
end
|
364
|
-
auth_resp.errback { |err| response.fail err }
|
365
|
-
response
|
346
|
+
def authenticate(username, password, auth_method=Authentication::AuthMethod::MONGODB_CR)
|
347
|
+
auth = case auth_method
|
348
|
+
when Authentication::AuthMethod::SCRAM_SHA1 then SCRAM.new self
|
349
|
+
when Authentication::AuthMethod::MONGODB_CR then MONGODB_CR.new self
|
350
|
+
else raise AuthenticationError.new("Authentication method #{auth_method} not supported")
|
351
|
+
end
|
352
|
+
return auth.authenticate(username, password)
|
366
353
|
end
|
367
354
|
|
368
355
|
# Adds a user to this database for use with authentication. If the user already
|
@@ -372,6 +359,7 @@ module EM::Mongo
|
|
372
359
|
# @param [String] password
|
373
360
|
#
|
374
361
|
# @return [EM::Mongo::RequestResponse] Calls back with an object representing the user.
|
362
|
+
# #TODO check if that works as it should with SCRAM-SHA1
|
375
363
|
def add_user(username, password)
|
376
364
|
response = RequestResponse.new
|
377
365
|
user_resp = self.collection(SYSTEM_USER_COLLECTION).first({:user => username})
|
data/lib/em-mongo/support.rb
CHANGED
@@ -16,23 +16,13 @@
|
|
16
16
|
# limitations under the License.
|
17
17
|
# ++
|
18
18
|
|
19
|
-
require '
|
19
|
+
require 'openssl'
|
20
20
|
|
21
21
|
module EM::Mongo
|
22
22
|
module Support
|
23
23
|
include EM::Mongo::Conversions
|
24
24
|
extend self
|
25
25
|
|
26
|
-
# Generate an MD5 for authentication.
|
27
|
-
#
|
28
|
-
# @param [String] username
|
29
|
-
# @param [String] password
|
30
|
-
# @param [String] nonce
|
31
|
-
#
|
32
|
-
# @return [String] a key for db authentication.
|
33
|
-
def auth_key(username, password, nonce)
|
34
|
-
Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}")
|
35
|
-
end
|
36
26
|
|
37
27
|
# Return a hashed password for auth.
|
38
28
|
#
|
@@ -41,7 +31,7 @@ module EM::Mongo
|
|
41
31
|
#
|
42
32
|
# @return [String]
|
43
33
|
def hash_password(username, plaintext)
|
44
|
-
Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}")
|
34
|
+
OpenSSL::Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}")
|
45
35
|
end
|
46
36
|
|
47
37
|
|
@@ -395,6 +395,19 @@ describe EMMongo::Collection do
|
|
395
395
|
|
396
396
|
end
|
397
397
|
|
398
|
+
describe "aggregate" do
|
399
|
+
it "should aggregate" do
|
400
|
+
@coll << { :a => 1, :b => 1 }
|
401
|
+
@coll << { :a => 2, :b => 1 }
|
402
|
+
@coll << { :a => 3, :b => 1 }
|
403
|
+
|
404
|
+
resp = @coll.aggregate([{'$project' => {:a => 1}}, {'$group' => {:_id => :counts, :counts => {'$sum' => 1} }}])
|
405
|
+
resp.callback do |doc|
|
406
|
+
doc[0]["counts"].should == 3
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
398
411
|
describe "mapreduce" do
|
399
412
|
it "should map, and then reduce" do
|
400
413
|
@conn, @coll = connection_and_collection
|
@@ -34,6 +34,18 @@ describe EMMongo::Database do
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
it "should create a collection with options" do
|
38
|
+
@conn = EM::Mongo::Connection.new
|
39
|
+
@db = @conn.db
|
40
|
+
@db.create_collection('capped', {:capped => true, :max => 10}).callback do |col|
|
41
|
+
@db.command({:collstats => 'capped'}).callback do |doc|
|
42
|
+
doc['capped'].should == 1
|
43
|
+
doc['max'].should == 10
|
44
|
+
done
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
37
49
|
it "should drop a collection" do
|
38
50
|
@conn = EM::Mongo::Connection.new
|
39
51
|
@db = @conn.db
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: em-mongo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- bcg
|
@@ -18,6 +18,9 @@ dependencies:
|
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
20
|
version: 0.12.10
|
21
|
+
- - "<"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '2.0'
|
21
24
|
type: :runtime
|
22
25
|
prerelease: false
|
23
26
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -25,20 +28,29 @@ dependencies:
|
|
25
28
|
- - ">="
|
26
29
|
- !ruby/object:Gem::Version
|
27
30
|
version: 0.12.10
|
31
|
+
- - "<"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
28
34
|
- !ruby/object:Gem::Dependency
|
29
35
|
name: bson
|
30
36
|
requirement: !ruby/object:Gem::Requirement
|
31
37
|
requirements:
|
32
|
-
- - "
|
38
|
+
- - ">="
|
33
39
|
- !ruby/object:Gem::Version
|
34
40
|
version: 1.9.2
|
41
|
+
- - "<"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '2.0'
|
35
44
|
type: :runtime
|
36
45
|
prerelease: false
|
37
46
|
version_requirements: !ruby/object:Gem::Requirement
|
38
47
|
requirements:
|
39
|
-
- - "
|
48
|
+
- - ">="
|
40
49
|
- !ruby/object:Gem::Version
|
41
50
|
version: 1.9.2
|
51
|
+
- - "<"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
42
54
|
description: EventMachine driver for MongoDB.
|
43
55
|
email: brenden.grace@gmail.com
|
44
56
|
executables: []
|
@@ -56,6 +68,9 @@ files:
|
|
56
68
|
- examples/legacy.rb
|
57
69
|
- examples/readme.rb
|
58
70
|
- lib/em-mongo.rb
|
71
|
+
- lib/em-mongo/auth/Authentication.rb
|
72
|
+
- lib/em-mongo/auth/mongodb_cr.rb
|
73
|
+
- lib/em-mongo/auth/scram.rb
|
59
74
|
- lib/em-mongo/collection.rb
|
60
75
|
- lib/em-mongo/connection.rb
|
61
76
|
- lib/em-mongo/conversions.rb
|
@@ -67,6 +82,7 @@ files:
|
|
67
82
|
- lib/em-mongo/request_response.rb
|
68
83
|
- lib/em-mongo/server_response.rb
|
69
84
|
- lib/em-mongo/support.rb
|
85
|
+
- lib/em-mongo/version.rb
|
70
86
|
- spec/gem/Gemfile
|
71
87
|
- spec/gem/bundler.rb
|
72
88
|
- spec/gem/rubygems.rb
|
@@ -97,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
113
|
version: '0'
|
98
114
|
requirements: []
|
99
115
|
rubyforge_project: em-mongo
|
100
|
-
rubygems_version: 2.2.
|
116
|
+
rubygems_version: 2.5.2.3
|
101
117
|
signing_key:
|
102
118
|
specification_version: 4
|
103
119
|
summary: An EventMachine driver for MongoDB.
|