From 9d1555604557b7ca385869a6fcebaf4dc0665059 Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Tue, 1 Jan 2013 18:34:23 +0100 Subject: [PATCH 01/10] Add credits module, more core improvements --- bot.rb | 15 +++++++++------ modules/Credits.rb | 17 +++++++++++++++++ modules/DDate.rb | 5 +---- 3 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 modules/Credits.rb diff --git a/bot.rb b/bot.rb index c6d96ce..80eb45e 100644 --- a/bot.rb +++ b/bot.rb @@ -6,17 +6,13 @@ require_relative 'sasl' require_relative 'config' module Lain + Version = '0.1' + class Bot - Version = '0.1' def initialize $stderr.puts "Lain #{Version} starting..." - @cl = Jabber::Client.new(Jabber::JID.new(Config::JID)) - @cl.connect - @cl.auth(Config::Password) - @cl.send(Jabber::Presence.new) - @modules = {} $stderr.puts 'Loading modules...' @@ -26,6 +22,13 @@ module Lain @modules[mod] = Modules.const_get(mod).new self } + $stderr.puts 'Connecting...' + + @cl = Jabber::Client.new(Jabber::JID.new(Config::JID)) + @cl.connect + @cl.auth(Config::Password) + @cl.send(Jabber::Presence.new) + @mucs = {} Config::Rooms.each { |r| diff --git a/modules/Credits.rb b/modules/Credits.rb new file mode 100644 index 0000000..e0289e4 --- /dev/null +++ b/modules/Credits.rb @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +require 'xmpp4r/message' + +require_relative '../module_base' + +module Lain + module Modules + class Credits < Base + def on_message(muc, message) + return unless message.type == :groupchat + return unless /!credits\b/ =~ message.body + + muc.send(Jabber::Message.new(message.to, "Lain (レイン) #{Version}\n\n -- hacked by NeoRaider")) + end + end + end +end diff --git a/modules/DDate.rb b/modules/DDate.rb index 1402648..af9912e 100644 --- a/modules/DDate.rb +++ b/modules/DDate.rb @@ -9,10 +9,7 @@ module Lain return unless message.type == :groupchat return unless /!ddate\b/ =~ message.body - p = IO.popen("ddate") - while (line = p.gets) - muc.send(Jabber::Message.new(message.to, line.chomp)) - end + muc.send(Jabber::Message.new(message.to, IO.popen("ddate").gets(sep=nil).chomp)) end end end From 09e925d6956cd7431a3466d8b659a7207e029617 Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Tue, 1 Jan 2013 19:03:27 +0100 Subject: [PATCH 02/10] Add Fortune module --- bot.rb | 4 ++-- module_base.rb | 3 ++- modules/Fortune.rb | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 modules/Fortune.rb diff --git a/bot.rb b/bot.rb index 80eb45e..6040029 100644 --- a/bot.rb +++ b/bot.rb @@ -17,9 +17,9 @@ module Lain $stderr.puts 'Loading modules...' - Config::Modules.each { |mod| + Config::Modules.each { |mod, cfg| require_relative "modules/#{mod}" - @modules[mod] = Modules.const_get(mod).new self + @modules[mod] = Modules.const_get(mod).new(self, cfg) } $stderr.puts 'Connecting...' diff --git a/module_base.rb b/module_base.rb index 500fd20..01d3f74 100644 --- a/module_base.rb +++ b/module_base.rb @@ -1,7 +1,8 @@ module Lain module Modules class Base - def initialize(lain) + def initialize(lain, config) + @config = config end def on_message(muc, message) diff --git a/modules/Fortune.rb b/modules/Fortune.rb new file mode 100644 index 0000000..2b9d63b --- /dev/null +++ b/modules/Fortune.rb @@ -0,0 +1,16 @@ +require 'xmpp4r/message' + +require_relative '../module_base' + +module Lain + module Modules + class Fortune < Base + def on_message(muc, message) + return unless message.type == :groupchat + return unless /!fortune\b/ =~ message.body + + muc.send(Jabber::Message.new(message.to, IO.popen(@config['command']).gets(sep=nil).chomp)) + end + end + end +end From dceabdbb7ede39694ecac9e71b36124e4123e741 Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Wed, 2 Jan 2013 01:13:29 +0100 Subject: [PATCH 03/10] Add command help --- bot.rb | 28 ++++++++++++++++++++-------- module_base.rb | 5 +++++ modules/Credits.rb | 8 +++++++- modules/DDate.rb | 8 +++++++- modules/Fortune.rb | 8 +++++++- modules/Help.rb | 22 ++++++++++++++++++++++ 6 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 modules/Help.rb diff --git a/bot.rb b/bot.rb index 6040029..524bb02 100644 --- a/bot.rb +++ b/bot.rb @@ -22,6 +22,8 @@ module Lain @modules[mod] = Modules.const_get(mod).new(self, cfg) } + @commands = @modules.values.reduce({}) { |c, mod| c.merge mod.commands }.to_a.sort + $stderr.puts 'Connecting...' @cl = Jabber::Client.new(Jabber::JID.new(Config::JID)) @@ -36,23 +38,33 @@ module Lain @mucs[r] = muc muc.add_message_callback { |msg| - @modules.each { | _, mod | - begin - mod.on_message muc, msg - rescue - end - } + unless msg.from == r + @modules.each { | _, mod | + begin + mod.on_message muc, msg + rescue + end + } + end } $stderr.puts "Joining room `#{r}'..." - muc.join("#{r}/#{Config::Nick}") - muc.configure() + muc.join(r) + + begin + muc.configure + rescue + end } $stderr.puts 'Startup finished.' end + def commands + @commands + end + def run @mainthread = Thread.current Thread.stop diff --git a/module_base.rb b/module_base.rb index 01d3f74..b209e7b 100644 --- a/module_base.rb +++ b/module_base.rb @@ -2,11 +2,16 @@ module Lain module Modules class Base def initialize(lain, config) + @lain = lain @config = config end def on_message(muc, message) end + + def commands + {} + end end end end diff --git a/modules/Credits.rb b/modules/Credits.rb index e0289e4..190c284 100644 --- a/modules/Credits.rb +++ b/modules/Credits.rb @@ -10,7 +10,13 @@ module Lain return unless message.type == :groupchat return unless /!credits\b/ =~ message.body - muc.send(Jabber::Message.new(message.to, "Lain (レイン) #{Version}\n\n -- hacked by NeoRaider")) + muc.send(Jabber::Message.new(message.to, "\nLain (レイン) #{Version}\n\n -- hacked by NeoRaider")) + end + + def commands + { + '!credits' => 'show credits' + } end end end diff --git a/modules/DDate.rb b/modules/DDate.rb index af9912e..3c9f3e1 100644 --- a/modules/DDate.rb +++ b/modules/DDate.rb @@ -9,7 +9,13 @@ module Lain return unless message.type == :groupchat return unless /!ddate\b/ =~ message.body - muc.send(Jabber::Message.new(message.to, IO.popen("ddate").gets(sep=nil).chomp)) + muc.send(Jabber::Message.new(message.to, IO.popen("ddate").read.chomp)) + end + + def commands + { + '!ddate' => 'show Discordian date' + } end end end diff --git a/modules/Fortune.rb b/modules/Fortune.rb index 2b9d63b..f8b237a 100644 --- a/modules/Fortune.rb +++ b/modules/Fortune.rb @@ -9,7 +9,13 @@ module Lain return unless message.type == :groupchat return unless /!fortune\b/ =~ message.body - muc.send(Jabber::Message.new(message.to, IO.popen(@config['command']).gets(sep=nil).chomp)) + muc.send(Jabber::Message.new(message.to, "\n" + IO.popen(@config['command']).read.chomp)) + end + + def commands + { + '!fortune' => 'fortune cookies' + } end end end diff --git a/modules/Help.rb b/modules/Help.rb new file mode 100644 index 0000000..84d9a9e --- /dev/null +++ b/modules/Help.rb @@ -0,0 +1,22 @@ +require 'xmpp4r/message' + +require_relative '../module_base' + +module Lain + module Modules + class Help < Base + def on_message(muc, message) + return unless message.type == :groupchat + return unless /!help\b/ =~ message.body + + muc.send(Jabber::Message.new(message.to, "Commands:" + @lain.commands.reduce('') { |s, cmd| "#{s}\n#{cmd[0]}: #{cmd[1]}" })) + end + + def commands + { + '!help' => 'show this help' + } + end + end + end +end From 58649c9843707c5d8b4211d07535fcf51421ebec Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Wed, 2 Jan 2013 02:16:57 +0100 Subject: [PATCH 04/10] Add Topic module --- bot.rb | 4 +-- modules/Credits.rb | 5 ++- modules/DDate.rb | 5 ++- modules/Fortune.rb | 5 ++- modules/Help.rb | 5 ++- modules/Topic.rb | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 modules/Topic.rb diff --git a/bot.rb b/bot.rb index 524bb02..6a4065b 100644 --- a/bot.rb +++ b/bot.rb @@ -1,5 +1,5 @@ require 'xmpp4r' -require 'xmpp4r/muc/helper/mucclient' +require 'xmpp4r/muc/helper/simplemucclient' require_relative 'sasl' @@ -34,7 +34,7 @@ module Lain @mucs = {} Config::Rooms.each { |r| - muc = Jabber::MUC::MUCClient.new(@cl) + muc = Jabber::MUC::SimpleMUCClient.new(@cl) @mucs[r] = muc muc.add_message_callback { |msg| diff --git a/modules/Credits.rb b/modules/Credits.rb index 190c284..972a13d 100644 --- a/modules/Credits.rb +++ b/modules/Credits.rb @@ -7,10 +7,9 @@ module Lain module Modules class Credits < Base def on_message(muc, message) - return unless message.type == :groupchat - return unless /!credits\b/ =~ message.body + return unless /\A!credits\b/ =~ message.body - muc.send(Jabber::Message.new(message.to, "\nLain (レイン) #{Version}\n\n -- hacked by NeoRaider")) + muc.send(Jabber::Message.new(muc.room, "\nLain (レイン) #{Version}\n\n -- coded by NeoRaider")) end def commands diff --git a/modules/DDate.rb b/modules/DDate.rb index 3c9f3e1..317bf67 100644 --- a/modules/DDate.rb +++ b/modules/DDate.rb @@ -6,10 +6,9 @@ module Lain module Modules class DDate < Base def on_message(muc, message) - return unless message.type == :groupchat - return unless /!ddate\b/ =~ message.body + return unless /\A!ddate\b/ =~ message.body - muc.send(Jabber::Message.new(message.to, IO.popen("ddate").read.chomp)) + muc.send(Jabber::Message.new(muc.room, IO.popen("ddate").read.chomp)) end def commands diff --git a/modules/Fortune.rb b/modules/Fortune.rb index f8b237a..85a5cba 100644 --- a/modules/Fortune.rb +++ b/modules/Fortune.rb @@ -6,10 +6,9 @@ module Lain module Modules class Fortune < Base def on_message(muc, message) - return unless message.type == :groupchat - return unless /!fortune\b/ =~ message.body + return unless /\A!fortune\b/ =~ message.body - muc.send(Jabber::Message.new(message.to, "\n" + IO.popen(@config['command']).read.chomp)) + muc.send(Jabber::Message.new(muc.room, "\n" + IO.popen(@config['command']).read.chomp)) end def commands diff --git a/modules/Help.rb b/modules/Help.rb index 84d9a9e..7c27689 100644 --- a/modules/Help.rb +++ b/modules/Help.rb @@ -6,10 +6,9 @@ module Lain module Modules class Help < Base def on_message(muc, message) - return unless message.type == :groupchat - return unless /!help\b/ =~ message.body + return unless /\A!help\b/ =~ message.body - muc.send(Jabber::Message.new(message.to, "Commands:" + @lain.commands.reduce('') { |s, cmd| "#{s}\n#{cmd[0]}: #{cmd[1]}" })) + muc.send(Jabber::Message.new(muc.room, "Commands:" + @lain.commands.reduce('') { |s, cmd| "#{s}\n#{cmd[0]}: #{cmd[1]}" })) end def commands diff --git a/modules/Topic.rb b/modules/Topic.rb new file mode 100644 index 0000000..14bd9c4 --- /dev/null +++ b/modules/Topic.rb @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +require 'xmpp4r/message' + +require_relative '../module_base' + +module Lain + module Modules + class Topic < Base + def on_message(muc, message) + return unless /\A!topic\b/ =~ message.body + + if /\A!topic\s*(?:\ss(?:how)?.*)?\Z/ =~ message.body + show_topic(muc, message) + elsif /\A!topic\s+a(?:dd)?\s+(.+)\Z/ =~ message.body + add_topic(muc, message, $~[1].strip) + elsif /\A!topic\s+d(?:el(?:ete)?)?\s+(\d+)\Z/ =~ message.body + del_topic(muc, message, $~[1].to_i) + end + end + + def commands + { + '!topic [s[how]]' => 'show topics (with IDs)', + '!topic a[dd] ' => 'add a topic to the topic line', + '!topic d[el[ete]] ' => 'remove topic ' + } + end + + private + + def current_topic(muc) + topic = muc.subject + return [] if topic.nil? + + topic.split('|').map { |s| s.strip } + end + + def set_topic(muc, topic) + muc.subject = topic.join(" | ") + end + + def topic_list(topic) + topic.each_with_index.map { |t, i| "[#{i}] #{t}" }.join("\n") + end + + def show_topic(muc, message) + topic = current_topic muc + if topic.empty? + muc.send(Jabber::Message.new(muc.room, "No topic set.")) + return + end + + muc.send(Jabber::Message.new(muc.room, "Current topics:\n" + topic_list(topic))) + end + + def add_topic(muc, message, text) + topic = current_topic muc + topic.unshift text + set_topic(muc, topic) + + muc.send(Jabber::Message.new(muc.room, "New topics:\n" + topic_list(topic))) + end + + def del_topic(muc, message, id) + topic = current_topic muc + begin + topic.delete_at id + rescue + end + set_topic(muc, topic) + + muc.send(Jabber::Message.new(muc.room, "New topics:\n" + topic_list(topic))) + end + end + end +end From 4b3975fc0a495ea18ae3bcaf0fbf5ee2c924ed8c Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Wed, 2 Jan 2013 05:06:04 +0100 Subject: [PATCH 05/10] Adjust strings. --- bot.rb | 17 ++++++++++++----- modules/Credits.rb | 7 ++++++- modules/DDate.rb | 2 +- modules/Fortune.rb | 2 +- modules/Help.rb | 2 +- modules/Topic.rb | 41 ++++++++++++++++++++++++++--------------- 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/bot.rb b/bot.rb index 6a4065b..55fbac7 100644 --- a/bot.rb +++ b/bot.rb @@ -11,26 +11,31 @@ module Lain class Bot def initialize - $stderr.puts "Lain #{Version} starting..." + $stderr.puts "Lain #{Version}" @modules = {} - $stderr.puts 'Loading modules...' + $stderr.print 'Loading modules...' Config::Modules.each { |mod, cfg| + $stderr.print " #{mod}" require_relative "modules/#{mod}" @modules[mod] = Modules.const_get(mod).new(self, cfg) } + $stderr.puts '.' + @commands = @modules.values.reduce({}) { |c, mod| c.merge mod.commands }.to_a.sort - $stderr.puts 'Connecting...' + $stderr.print 'Connecting... ' @cl = Jabber::Client.new(Jabber::JID.new(Config::JID)) @cl.connect @cl.auth(Config::Password) @cl.send(Jabber::Presence.new) + $stderr.puts 'connection established.' + @mucs = {} Config::Rooms.each { |r| @@ -48,7 +53,7 @@ module Lain end } - $stderr.puts "Joining room `#{r}'..." + $stderr.print "Trying to access room `#{r}'... " muc.join(r) @@ -56,9 +61,11 @@ module Lain muc.configure rescue end + + $stderr.puts "access granted." } - $stderr.puts 'Startup finished.' + $stderr.puts 'Initialization finished.' end def commands diff --git a/modules/Credits.rb b/modules/Credits.rb index 972a13d..ad2bd4a 100644 --- a/modules/Credits.rb +++ b/modules/Credits.rb @@ -9,7 +9,12 @@ module Lain def on_message(muc, message) return unless /\A!credits\b/ =~ message.body - muc.send(Jabber::Message.new(muc.room, "\nLain (レイン) #{Version}\n\n -- coded by NeoRaider")) + muc.say < 'show topics (with IDs)', - '!topic a[dd] ' => 'add a topic to the topic line', - '!topic d[el[ete]] ' => 'remove topic ' + '!topic [s[how]]' => 'show topics (with indices)', + '!topic a[dd] ' => 'add a topic', + '!topic d[el[ete]] ' => 'remove topic ' } end @@ -40,36 +40,47 @@ module Lain end def topic_list(topic) - topic.each_with_index.map { |t, i| "[#{i}] #{t}" }.join("\n") + topic.each_with_index.map { |t, i| "[#{'%02x' % i}] #{t}" }.join("\n") end def show_topic(muc, message) topic = current_topic muc if topic.empty? - muc.send(Jabber::Message.new(muc.room, "No topic set.")) + muc.say "\nTOPIC EMPTY" return end - muc.send(Jabber::Message.new(muc.room, "Current topics:\n" + topic_list(topic))) + muc.say("\nBEGIN TOPIC LIST\n" + topic_list(topic) + "\nEND TOPIC LIST") end def add_topic(muc, message, text) topic = current_topic muc - topic.unshift text - set_topic(muc, topic) + if text.empty? + pre = "\nERROR: No topic given." + else + topic.unshift text + set_topic(muc, topic) + pre = "\nSUCCESS" + end - muc.send(Jabber::Message.new(muc.room, "New topics:\n" + topic_list(topic))) + muc.say(pre + "\n\nBEGIN TOPIC LIST\n" + topic_list(topic) + "\nEND TOPIC LIST") end - def del_topic(muc, message, id) + def del_topic(muc, message, index) topic = current_topic muc begin - topic.delete_at id + deleted = topic.delete_at index rescue + deleted = nil + end + if deleted.nil? + pre = "\nERROR: Invalid index [#{'%02x' % index}]." + else + set_topic(muc, topic) + pre = "\nSUCCESS" end - set_topic(muc, topic) - muc.send(Jabber::Message.new(muc.room, "New topics:\n" + topic_list(topic))) + muc.say(pre + "\n\nBEGIN TOPIC LIST\n" + topic_list(topic) + "\nEND TOPIC LIST") end end end From 1ed0e06c2fefedc7419b611bba405f6ec5ab99cc Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Wed, 2 Jan 2013 05:18:19 +0100 Subject: [PATCH 06/10] Update credits --- modules/Credits.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/Credits.rb b/modules/Credits.rb index ad2bd4a..1df7d18 100644 --- a/modules/Credits.rb +++ b/modules/Credits.rb @@ -6,15 +6,18 @@ require_relative '../module_base' module Lain module Modules class Credits < Base - def on_message(muc, message) - return unless /\A!credits\b/ =~ message.body - - muc.say < Date: Wed, 2 Jan 2013 05:18:31 +0100 Subject: [PATCH 07/10] Some finishing touches for the first public version --- .gitignore | 1 + config.rb.example | 16 ++++++++++++++++ lain.rb | 0 3 files changed, 17 insertions(+) create mode 100644 config.rb.example mode change 100644 => 100755 lain.rb diff --git a/.gitignore b/.gitignore index b25c15b..f74e8fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *~ +config.rb diff --git a/config.rb.example b/config.rb.example new file mode 100644 index 0000000..a853746 --- /dev/null +++ b/config.rb.example @@ -0,0 +1,16 @@ +module Lain + module Config + JID = '' + Password = '' + + Rooms = [''] + + Modules = { + 'Credits' => {}, + 'DDate' => {}, + 'Fortune' => { 'command' => '/usr/bin/fortune -a' }, + 'Help' => {}, + 'Topic' => {}, + } + end +end diff --git a/lain.rb b/lain.rb old mode 100644 new mode 100755 From 09d8506f82cab613a7fead41328d501cadd5135a Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Thu, 3 Jan 2013 08:35:03 +0100 Subject: [PATCH 08/10] Use symbols for module configuration keys --- config.rb.example | 2 +- modules/Fortune.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.rb.example b/config.rb.example index a853746..995a262 100644 --- a/config.rb.example +++ b/config.rb.example @@ -8,7 +8,7 @@ module Lain Modules = { 'Credits' => {}, 'DDate' => {}, - 'Fortune' => { 'command' => '/usr/bin/fortune -a' }, + 'Fortune' => { :command => '/usr/bin/fortune -a' }, 'Help' => {}, 'Topic' => {}, } diff --git a/modules/Fortune.rb b/modules/Fortune.rb index 9a762b4..2d4436b 100644 --- a/modules/Fortune.rb +++ b/modules/Fortune.rb @@ -8,7 +8,7 @@ module Lain def on_message(muc, message) return unless /\A!fortune\b/ =~ message.body - muc.say("\nBEGIN FORTUNE COOKIE\n" + IO.popen(@config['command']).read.chomp + "\nEND FORTUNE COOKIE") + muc.say("\nBEGIN FORTUNE COOKIE\n" + IO.popen(@config[:command]).read.chomp + "\nEND FORTUNE COOKIE") end def commands From 46099da79330934c807c2b3ad5eef2f0174a8592 Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Thu, 3 Jan 2013 08:40:22 +0100 Subject: [PATCH 09/10] Use symbols for module names --- config.rb.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config.rb.example b/config.rb.example index 995a262..00242b3 100644 --- a/config.rb.example +++ b/config.rb.example @@ -6,11 +6,11 @@ module Lain Rooms = [''] Modules = { - 'Credits' => {}, - 'DDate' => {}, - 'Fortune' => { :command => '/usr/bin/fortune -a' }, - 'Help' => {}, - 'Topic' => {}, + :Credits => {}, + :DDate => {}, + :Fortune => { :command => '/usr/bin/fortune -a' }, + :Help => {}, + :Topic => {}, } end end From 5544cc1a47ee2cbcdfd5b5596fb82a3986fe8cdd Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Mon, 25 Mar 2013 19:55:15 +0100 Subject: [PATCH 10/10] Remove monkeypatched SCRAM-SHA-1 implementation --- bot.rb | 2 - sasl.rb | 201 -------------------------------------------------------- 2 files changed, 203 deletions(-) delete mode 100644 sasl.rb diff --git a/bot.rb b/bot.rb index 55fbac7..1d5c0f6 100644 --- a/bot.rb +++ b/bot.rb @@ -1,8 +1,6 @@ require 'xmpp4r' require 'xmpp4r/muc/helper/simplemucclient' -require_relative 'sasl' - require_relative 'config' module Lain diff --git a/sasl.rb b/sasl.rb deleted file mode 100644 index f871533..0000000 --- a/sasl.rb +++ /dev/null @@ -1,201 +0,0 @@ -require 'digest/sha1' -require 'openssl' -require 'xmpp4r/client' -require 'xmpp4r/sasl' - - -module Jabber - class Client - def auth(password) - #begin - if @stream_mechanisms.include? 'SCRAM-SHA-1' - auth_sasl SASL.new(self, 'SCRAM-SHA-1'), password - elsif @stream_mechanisms.include? 'DIGEST-MD5' - auth_sasl SASL.new(self, 'DIGEST-MD5'), password - elsif @stream_mechanisms.include? 'PLAIN' - auth_sasl SASL.new(self, 'PLAIN'), password - else - auth_nonsasl(password) - end - #rescue - # Jabber::debuglog("#{$!.class}: #{$!}\n#{$!.backtrace.join("\n")}") - # raise ClientAuthenticationFailure.new, $!.to_s - #end - end - end - - module SASL - ## - # Factory function to obtain a SASL helper for the specified mechanism - def SASL.new(stream, mechanism) - case mechanism - when 'SCRAM-SHA-1' - SCRAMSHA1.new(stream) - when 'DIGEST-MD5' - DigestMD5.new(stream) - when 'PLAIN' - Plain.new(stream) - when 'ANONYMOUS' - Anonymous.new(stream) - else - raise "Unknown SASL mechanism: #{mechanism}" - end - end - - ## - # SASL SCRAM-SHA1 authentication helper - class SCRAMSHA1 < Base - ## - # Sends the wished auth mechanism and wait for a challenge - # - # (proceed with SCRAMSHA1#auth) - def initialize(stream) - super - - @nonce = generate_nonce - @client_fm = "n=#{escape @stream.jid.node },r=#{@nonce}" - - challenge = {} - challenge_text = '' - error = nil - @stream.send(generate_auth('SCRAM-SHA-1', text=Base64::strict_encode64('n,,'+@client_fm))) { |reply| - if reply.name == 'challenge' and reply.namespace == NS_SASL - challenge_text = Base64::decode64(reply.text) - challenge = decode_challenge(challenge_text) - else - error = reply.first_element(nil).name - end - true - } - raise error if error - - @server_fm = challenge_text - @cnonce = challenge['r'] - @salt = Base64::decode64(challenge['s']) - @iterations = challenge['i'].to_i - - raise 'SCRAM-SHA-1 protocol error' if @cnonce[0, @nonce.length] != @nonce - end - - def decode_challenge(text) - res = {} - - state = :key - key = '' - value = '' - text.scan(/./) do |ch| - if state == :key - if ch == '=' - state = :value - else - key += ch - end - - elsif state == :value - if ch == ',' - # due to our home-made parsing of the challenge, the key could have - # leading whitespace. strip it, or that would break jabberd2 support. - key = key.strip - res[key] = value - key = '' - value = '' - state = :key - elsif ch == '"' and value == '' - state = :quote - else - value += ch - end - - elsif state == :quote - if ch == '"' - state = :value - else - value += ch - end - end - end - # due to our home-made parsing of the challenge, the key could have - # leading whitespace. strip it, or that would break jabberd2 support. - key = key.strip - res[key] = value unless key == '' - - Jabber::debuglog("SASL SCRAM-SHA-1 challenge:\n#{text}\n#{res.inspect}") - - res - end - - ## - # * Send a response - # * Wait for the server's challenge (which aren't checked) - # * Send a blind response to the server's challenge - def auth(password) - salted_password = hi(password, @salt, @iterations) - client_key = hmac(salted_password, 'Client Key') - stored_key = h(client_key) - - final_message = "c=#{Base64::strict_encode64('n,,')},r=#{@cnonce}" - auth_message = "#{@client_fm},#{@server_fm},#{final_message}" - - client_signature = hmac(stored_key, auth_message) - client_proof = xor(client_key, client_signature) - - - response_text = "#{final_message},p=#{Base64::strict_encode64(client_proof)}" - - Jabber::debuglog("SASL SCRAM-SHA-1 response:\n#{response_text}") - - r = REXML::Element.new('response') - r.add_namespace NS_SASL - r.text = Base64::strict_encode64(response_text) - - error = nil - success = {} - @stream.send(r) { |reply| - if reply.name == 'success' and reply.namespace == NS_SASL - success = decode_challenge(Base64::decode64(reply.text)) - elsif reply.name != 'challenge' - error = reply.first_element(nil).name - end - true - } - - raise error if error - - server_key = hmac(salted_password, 'Server Key') - server_signature = hmac(server_key, auth_message) - - raise "Server authentication failed" if Base64::decode64(success['v']) != server_signature - end - - private - - def xor(a, b) - a.unpack('C*').zip(b.unpack('C*')).collect { | x, y | x ^ y }.pack('C*') - end - - def h(s) - Digest::SHA1.digest(s) - end - - def hmac(key, s) - OpenSSL::HMAC.digest('sha1', key, s) - end - - def hi(s, salt, i) - r = Array.new(size=20, obj=0).pack('C*') - u = salt + [0, 0, 0, 1].pack('C*') - - i.times do |x| - u = hmac(s, u) - r = xor(r, u) - end - - r - end - - def escape(data) - data.gsub(/=/, '=3D').gsub(/,/, '=2C') - end - end - end -end