Repository navigation
Expand file tree
/
Copy pathstorage.rb
More file actions
316 lines (295 loc) · 10 KB
/
Copy pathstorage.rb
File metadata and controls
316 lines (295 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# encoding: UTF-8
module Vines
class Storage
include Vines::Log
attr_accessor :ldap
@@nicks = {}
# Register a nickname that can be used in the config file to specify this
# storage implementation.
#
# name - The String name for this storage backend.
#
# Returns nothing.
def self.register(name)
@@nicks[name.to_sym] = self
end
def self.from_name(name, &block)
klass = @@nicks[name.to_sym]
raise "#{name} storage class not found" unless klass
klass.new(&block)
end
# Wrap a blocking IO method in a new method that pushes the original method
# onto EventMachine's thread pool using EM#defer. Storage classes implemented
# with blocking IO don't need to worry about threading or blocking the
# EventMachine reactor thread if they wrap their methods with this one.
#
# Examples
#
# def find_user(jid)
# some_blocking_lookup(jid)
# end
# defer :find_user
#
# Storage classes that use asynchronous IO (through an EventMachine
# enabled library like em-http-request or em-redis) don't need any special
# consideration and must not use this method.
#
# Returns nothing.
def self.defer(method)
old = instance_method(method)
define_method method do |*args|
fiber = Fiber.current
op = operation { old.bind(self).call(*args) }
cb = proc {|result| fiber.resume(result) }
EM.defer(op, cb)
Fiber.yield
end
end
# Wrap an authenticate method with a new method that uses LDAP if it's
# enabled in the config file. If LDAP is not enabled, invoke the original
# authenticate method as usual. This allows storage classes to implement
# their native authentication logic and not worry about handling LDAP.
#
# Examples
#
# def authenticate(username, password)
# some_user_lookup_by_password(username, password)
# end
# wrap_ldap :authenticate
#
# Returns nothing.
def self.wrap_ldap(method)
old = instance_method(method)
define_method method do |*args|
ldap? ? authenticate_with_ldap(*args) : old.bind(self).call(*args)
end
end
# Wrap a method with Fiber yield and resume logic. The method must yield
# its result to a block. This makes it easier to write asynchronous
# implementations of `authenticate`, `find_user`, and `save_user` that
# block and return a result rather than yielding.
#
# Examples
#
# def find_user(jid)
# http = EM::HttpRequest.new(url).get
# http.callback { yield build_user_from_http_response(http) }
# end
# fiber :find_user
#
# Because `find_user` has been wrapped in Fiber logic, we can call it
# synchronously even though it uses asynchronous EventMachine calls.
#
# user = storage.find_user('alice@wonderland.lit')
# puts user.nil?
#
# Returns nothing.
def self.fiber(method)
old = instance_method(method)
define_method method do |*args|
fiber, yielding = Fiber.current, true
old.bind(self).call(*args) do |user|
fiber.resume(user) rescue yielding = false
end
Fiber.yield if yielding
end
end
# Return true if users are authenticated against an LDAP directory.
def ldap?
!!ldap
end
# Validate the username and password pair.
#
# username - The String login JID to verify.
# password - The String password the user presented to the server.
#
# Examples
#
# user = storage.authenticate('alice@wonderland.lit', 'secr3t')
# puts user.nil?
#
# This default implementation validates the password against a bcrypt hash
# of the password stored in the database. Sub-classes not using bcrypt
# passwords must override this method.
#
# Returns a Vines::User object on success, nil on failure.
def authenticate(username, password)
user = find_user(username)
hash = BCrypt::Password.new(user.password) rescue nil
(hash && hash == password) ? user : nil
end
wrap_ldap :authenticate
# Find the user in the storage database by their unique JID.
#
# jid - The String or JID of the user, possibly nil. This may be either a
# bare JID or full JID. Implementations of this method must convert
# the JID to a bare JID before searching for the user in the database.
#
# Examples
#
# # Bare JID lookup.
# user = storage.find_user('alice@wonderland.lit')
# puts user.nil?
#
# # Full JID lookup.
# user = storage.find_user('alice@wonderland.lit/tea')
# puts user.nil?
#
# Returns the User identified by the JID, nil if not found.
def find_user(jid)
raise 'subclass must implement'
end
# Persist the user to the database, and return when the save is complete.
#
# user - The User to persist.
#
# Examples
#
# alice = Vines::User.new(jid: 'alice@wonderland.lit')
# storage.save_user(alice)
# puts 'saved'
#
# Returns nothing.
def save_user(user)
raise 'subclass must implement'
end
# Find the user's vcard by their unique JID.
#
# jid - The String or JID of the user, possibly nil. This may be either a
# bare JID or full JID. Implementations of this method must convert
# the JID to a bare JID before searching for the vcard in the database.
#
# Examples
#
# card = storage.find_vcard('alice@wonderland.lit')
# puts card.nil?
#
# Returns the vcard's Nokogiri::XML::Node, nil if not found.
def find_vcard(jid)
raise 'subclass must implement'
end
# Save the vcard to the database, and return when the save is complete.
#
# jid - The String or JID of the user, possibly nil. This may be either a
# bare JID or full JID. Implementations of this method must convert
# the JID to a bare JID before saving the vcard.
# card - The vcard's Nokogiri::XML::Node.
#
# Examples
#
# card = Nokogiri::XML('<vCard>...</vCard>').root
# storage.save_vcard('alice@wonderland.lit', card)
# puts 'saved'
#
# Returns nothing.
def save_vcard(jid, card)
raise 'subclass must implement'
end
# Find the private XML fragment previously stored by the user. Private
# XML storage uniquely identifies fragments by JID, root element name,
# and root element namespace.
#
# jid - The String or JID of the user, possibly nil. This may be either a
# bare JID or full JID. Implementations of this method must convert
# the JID to a bare JID before searching for the fragment in the database.
# node - The XML::Node that uniquely identifies the fragment by element
# name and namespace.
#
# Examples
#
# root = Nokogiri::XML('<custom xmlns="urn:custom:ns"/>').root
# fragment = storage.find_fragment('alice@wonderland.lit', root)
# puts fragment.nil?
#
# Returns the fragment's Nokogiri::XML::Node or nil if not found.
def find_fragment(jid, node)
raise 'subclass must implement'
end
# Save the XML fragment to the database, and return when the save is complete.
#
# jid - The String or JID of the user, possibly nil. This may be
# either a bare JID or full JID. Implementations of this method
# must convert the JID to a bare JID before searching for the
# fragment.
# fragment - The XML::Node to save.
#
# Examples
#
# fragment = Nokogiri::XML('<custom xmlns="urn:custom:ns">some data</custom>').root
# storage.save_fragment('alice@wonderland.lit', fragment)
# puts 'saved'
#
# Returns nothing.
def save_fragment(jid, fragment)
raise 'subclass must implement'
end
private
# Determine if any of the arguments are nil or empty strings.
#
# Examples
#
# username, password = 'alice@wonderland.lit', ''
# empty?(username, password) #=> true
#
# Returns true if any of the arguments are nil or empty strings.
def empty?(*args)
args.flatten.any? {|arg| (arg || '').strip.empty? }
end
# Create a proc suitable for running on the EM.defer thread pool, that
# traps and logs any errors thrown by the provided block.
#
# block - The block to wrap in error handling.
#
# Examples
#
# op = operation { do_something_on_thread_pool() }
# EM.defer(op)
#
# Returns a Proc.
def operation
proc do
begin
yield
rescue => e
log.error("Thread pool operation failed: #{e.message}")
nil
end
end
end
# Bind to the LDAP server using the provided username and password. If
# authentication succeeds, but the user is not yet stored in our database,
# save the user to the database.
#
# username - The String JID to authenticate.
# password - The String password the user provided.
# block - The block that receives the authenticated User or nil.
#
# Returns the authenticated User or nil if authentication failed.
def authenticate_with_ldap(username, password, &block)
op = operation { ldap.authenticate(username, password) }
cb = proc {|user| save_ldap_user(user, &block) }
EM.defer(op, cb)
end
fiber :authenticate_with_ldap
# Save missing users to the storage database after they're authenticated
# with LDAP. This allows admins to define users once in LDAP and have them
# sync to the chat database the first time they successfully sign in.
#
# user - The User to persist, possibly nil.
# block - The block that receives the saved User, possibly nil.
#
# Returns nothing.
def save_ldap_user(user, &block)
Fiber.new do
if user.nil?
block.call
elsif found = find_user(user.jid)
block.call(found)
else
save_user(user)
block.call(user)
end
end.resume
end
end
end