# 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.
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 the
# EventMachine reactor thread if they wrap their methods with this one.
#
# For example:
# 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.
def self.defer(method)
old = "_deferred_#{method}"
alias_method old, method
define_method method do |*args|
fiber = Fiber.current
op = proc do
begin
method(old).call(*args)
rescue Exception => e
log.error("Thread pool operation failed: #{e.message}")
nil
end
end
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.
#
# For example:
# def authenticate(username, password)
# some_user_lookup_by_password(username, password)
# end
# wrap_ldap :authenticate
def self.wrap_ldap(method)
old = "_ldap_#{method}"
alias_method old, method
define_method method do |*args|
ldap? ? authenticate_with_ldap(*args) : method(old).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.
#
# For example:
# 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?
def self.fiber(method)
old = "_fiber_#{method}"
alias_method old, method
define_method method do |*args|
fiber, yielding = Fiber.current, true
method(old).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 and return a Vines::User object
# on success. Return nil on failure.
#
# For example:
# 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.
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
# Return the Vines::User associated with the JID. Return nil if the user
# could not be found. JID may be +nil+, a +String+, or a +Vines::JID+
# object. It may be a bare JID or a full JID. Implementations of this method
# must convert the JID to a bare JID before searching for the user in the
# database.
#
# user = storage.find_user('alice@wonderland.lit')
# puts user.nil?
def find_user(jid)
raise 'subclass must implement'
end
# Persist the Vines::User object to the database and return when the save
# is complete.
#
# alice = Vines::User.new(:jid => 'alice@wonderland.lit')
# storage.save_user(alice)
# puts 'saved'
def save_user(user)
raise 'subclass must implement'
end
# Return the Nokogiri::XML::Node for the vcard stored for this JID. Return
# nil if the vcard could not be found. JID may be +nil+, a +String+, or a
# +Vines::JID+ object. It may be a bare JID or a full JID. Implementations
# of this method must convert the JID to a bare JID before searching for the
# vcard in the database.
#
# card = storage.find_vcard('alice@wonderland.lit')
# puts card.nil?
def find_vcard(jid)
raise 'subclass must implement'
end
# Save the vcard to the database and return when the save is complete. JID
# may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
# full JID. Implementations of this method must convert the JID to a bare
# JID before saving the vcard. Card is a +Nokogiri::XML::Node+ object.
#
# card = Nokogiri::XML('...').root
# storage.save_vcard('alice@wonderland.lit', card)
# puts 'saved'
def save_vcard(jid, card)
raise 'subclass must implement'
end
# Return the Nokogiri::XML::Node for the XML fragment stored for this JID.
# Return nil if the fragment could not be found. JID may be +nil+, a
# +String+, or a +Vines::JID+ object. It may be a bare JID or a full JID.
# Implementations of this method must convert the JID to a bare JID before
# searching for the fragment in the database.
#
# Private XML storage uniquely identifies fragments by JID, root element name,
# and root element namespace.
#
# root = Nokogiri::XML('').root
# fragment = storage.find_fragment('alice@wonderland.lit', root)
# puts fragment.nil?
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 may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
# full JID. Implementations of this method must convert the JID to a bare
# JID before saving the fragment. Fragment is a +Nokogiri::XML::Node+ object.
#
# fragment = Nokogiri::XML('some data').root
# storage.save_fragment('alice@wonderland.lit', fragment)
# puts 'saved'
def save_fragment(jid, fragment)
raise 'subclass must implement'
end
private
# Return true if any of the arguments are nil or empty strings.
# For example:
# username, password = 'alice@wonderland.lit', ''
# empty?(username, password) #=> true
def empty?(*args)
args.flatten.any? {|arg| (arg || '').strip.empty? }
end
# Return a Vines::User object if we are able to bind to the LDAP server
# using the username and password. Return nil if authentication failed. If
# authentication succeeds, but the user is not yet stored in our database,
# save the user to the database.
def authenticate_with_ldap(username, password, &block)
if empty?(username, password)
block.call; return
end
op = proc do
begin
ldap.authenticate(username, password)
rescue Exception => e
log.error("LDAP authentication failed: #{e.message}")
nil
end
end
cb = proc {|user| save_ldap_user(user, &block) }
EM.defer(op, cb)
end
fiber :authenticate_with_ldap
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