module Blather # :nodoc:
class Stream # :nodoc:
class SASL < StreamHandler # :nodoc:
class UnknownMechanism < BlatherError
handler_heirarchy ||= []
handler_heirarchy << :unknown_mechanism
end
SASL_NS = 'urn:ietf:params:xml:ns:xmpp-sasl'.freeze
def initialize(stream, jid, pass = nil)
super stream
@jid = jid
@pass = pass
@mechanism_idx = 0
@mechanisms = []
end
def set_mechanism
mod = case (mechanism = @mechanisms[@mechanism_idx].content)
when 'DIGEST-MD5' then DigestMD5
when 'PLAIN' then Plain
when 'ANONYMOUS' then Anonymous
else
# Send a failure node and kill the stream
@stream.send ""
@failure.call UnknownMechanism.new("Unknown SASL mechanism (#{mechanism})")
return false
end
extend mod
true
end
##
# Handle incoming nodes
# Cycle through possible mechanisms until we either
# run out of them or none work
def handle(node)
if node.element_name == 'failure'
if @mechanisms[@mechanism_idx += 1]
set_mechanism
authenticate
else
failure node
end
else
super
end
end
protected
def failure(node = nil)
@failure.call SASLError.import(node)
end
##
# Base64 Encoder
def b64(str)
[str].pack('m').gsub(/\s/,'')
end
##
# Builds a standard auth node
def auth_node(mechanism, content = nil)
node = XMPPNode.new 'auth', content
node['xmlns'] = SASL_NS
node['mechanism'] = mechanism
node
end
##
# Respond to the node sent by the server
def mechanisms
@mechanisms = @node.children
authenticate if set_mechanism
end
##
# Digest MD5 authentication
module DigestMD5 # :nodoc:
##
# Lets the server know we're going to try DigestMD5 authentication
def authenticate
@stream.send auth_node('DIGEST-MD5')
end
##
# Receive the challenge command.
def challenge
decode_challenge
respond
end
private
##
# Decodes digest strings 'foo=bar,baz="faz"'
# into {'foo' => 'bar', 'baz' => 'faz'}
def decode_challenge
text = @node.content.unpack('m').first
res = {}
text.split(',').each do |statement|
key, value = statement.split('=')
res[key] = value.delete('"') unless key.empty?
end
LOG.debug "CHALLENGE DECODE: #{res.inspect}"
@nonce ||= res['nonce']
@realm ||= res['realm']
end
##
# Builds the properly encoded challenge response
def generate_response
a1 = "#{d("#{@response[:username]}:#{@response[:realm]}:#{@pass}")}:#{@response[:nonce]}:#{@response[:cnonce]}"
a2 = "AUTHENTICATE:#{@response[:'digest-uri']}"
h("#{h(a1)}:#{@response[:nonce]}:#{@response[:nc]}:#{@response[:cnonce]}:#{@response[:qop]}:#{h(a2)}")
end
##
# Send challenge response
def respond
node = XMPPNode.new 'response'
node['xmlns'] = SASL_NS
unless @initial_response_sent
@initial_response_sent = true
@response = {
:nonce => @nonce,
:charset => 'utf-8',
:username => @jid.node,
:realm => @realm || @jid.domain,
:cnonce => h(Time.new.to_f.to_s),
:nc => '00000001',
:qop => 'auth',
:'digest-uri' => "xmpp/#{@jid.domain}",
}
@response[:response] = generate_response
@response.each { |k,v| @response[k] = "\"#{v}\"" unless [:nc, :qop, :response, :charset].include?(k) }
LOG.debug "CHALLENGE RESPOSNE: #{@response.inspect}"
LOG.debug "CH RESP TXT: #{@response.map { |k,v| "#{k}=#{v}" } * ','}"
# order is to simplify testing
# Ruby 1.9 eliminates the need for this with ordered hashes
order = [:nonce, :charset, :username, :realm, :cnonce, :nc, :qop, :'digest-uri', :response]
node.content = b64(order.map { |k| v = @response[k]; "#{k}=#{v}" } * ',')
end
@stream.send node
end
def d(s); Digest::MD5.digest(s); end
def h(s); Digest::MD5.hexdigest(s); end
end #DigestMD5
module Plain # :nodoc:
def authenticate
@stream.send auth_node('PLAIN', b64("#{@jid.stripped}\x00#{@jid.node}\x00#{@pass}"))
end
end #Plain
module Anonymous # :nodoc:
def authenticate
@stream.send auth_node('ANONYMOUS', b64(@jid.node))
end
end #Anonymous
end #SASL
end #Stream
end