em-mongo 0.5.1 → 0.6.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 +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.
|