From 495c44445642cfae8f23fadde299ad5307f5be58 Mon Sep 17 00:00:00 2001 From: Patrick J Cherry Date: Thu, 9 Jun 2011 18:09:52 +0100 Subject: Big commit --HG-- rename : views/please_authenticate.haml => views/login.haml --- lib/mauve/alert.rb | 82 +++++-- lib/mauve/alert_group.rb | 11 +- lib/mauve/auth_bytemark.rb | 10 +- lib/mauve/calendar_interface.rb | 8 +- lib/mauve/configuration.rb | 14 +- lib/mauve/http_server.rb | 16 +- lib/mauve/notification.rb | 7 +- lib/mauve/notifier.rb | 31 ++- lib/mauve/notifiers/email.rb | 22 +- lib/mauve/notifiers/xmpp.rb | 462 ++++++++++++++++++++++++++++------------ lib/mauve/people_list.rb | 6 +- lib/mauve/person.rb | 45 +++- lib/mauve/server.rb | 52 ++++- lib/mauve/source_list.rb | 2 +- lib/mauve/timer.rb | 8 +- lib/mauve/udp_server.rb | 10 +- lib/mauve/web_interface.rb | 220 +++++++++++++------ 17 files changed, 722 insertions(+), 284 deletions(-) (limited to 'lib/mauve') diff --git a/lib/mauve/alert.rb b/lib/mauve/alert.rb index 30d50bb..b98866c 100644 --- a/lib/mauve/alert.rb +++ b/lib/mauve/alert.rb @@ -103,7 +103,7 @@ module Mauve def logger Log4r::Logger.new(self.class.to_s) end - + def time_relative(secs) secs = secs.to_i.abs case secs @@ -167,6 +167,10 @@ module Mauve def alert_group AlertGroup.matches(self)[0] end + + def level + self.alert_group.level + end def subject attribute_get(:subject) || source @@ -174,7 +178,8 @@ module Mauve def subject=(subject); set_changed_if_different(:subject, subject); end def summary=(summary); set_changed_if_different(:summary, summary); end - def detail=(detail); set_changed_if_different(:detail, detail); end +# def detail=(detail); set_changed_if_different(:detail, detail); end + def detail=(detail); attribute_set(:detail, detail) ; end protected def set_changed_if_different(attribute, value) @@ -252,10 +257,33 @@ module Mauve class << self - def all_current - all(:cleared_at => nil) + def all_raised + all(:raised_at.not => nil, :cleared_at => nil) end - + + def all_acknowledged + all(:acknowledged_at.not => nil) + end + + def all_cleared + all(:cleared_at.not => nil) + end + + # Returns a hash of all the :urgent, :normal and :low alerts. + # + # @return [Hash] A hash with the relevant alerts per level + def get_all () + hash = Hash.new + hash[:urgent] = Array.new + hash[:normal] = Array.new + hash[:low] = Array.new + all().each do |iter| + next if true == iter.cleared? + hash[AlertGroup.matches(iter)[0].level] << iter + end + return hash + end + # Returns the next Alert that will have a timed action due on it, or nil # if none are pending. # @@ -273,8 +301,9 @@ module Mauve # Receive an AlertUpdate buffer from the wire. # def receive_update(update, reception_time = MauveTime.now) - update = Proto::AlertUpdate.parse_from_string(update) unless - update.kind_of?(Proto::AlertUpdate) + + update = Proto::AlertUpdate.parse_from_string(update) unless update.kind_of?(Proto::AlertUpdate) + alerts_updated = [] logger.debug("Alert update received from wire: #{update.inspect.split.join(", ")}") @@ -289,7 +318,8 @@ module Mauve end time_offset = (reception_time - transmission_time).round - logger.debug("Update received from a host #{time_offset}s behind") if time_offset.abs > 0 + + logger.debug("Update received from a host #{time_offset}s behind") if time_offset.abs > 5 # Update each alert supplied # @@ -297,8 +327,17 @@ module Mauve # Infer some actions from our pure data structure (hmm, wonder if # this belongs in our protobuf-derived class? # - raise_time = alert.raise_time == 0 ? nil : MauveTime.at(alert.raise_time + time_offset) clear_time = alert.clear_time == 0 ? nil : MauveTime.at(alert.clear_time + time_offset) + raise_time = alert.raise_time == 0 ? nil : MauveTime.at(alert.raise_time + time_offset) + + if raise_time.nil? && clear_time.nil? + # + # Make sure that we raise if neither raise nor clear is set + # + logger.warn("No clear time or raise time set. Assuming raised!") + + raise_time = reception_time + end logger.debug("received at #{reception_time}, transmitted at #{transmission_time}, raised at #{raise_time}, clear at #{clear_time}") @@ -316,10 +355,10 @@ module Mauve ## # - # Allow a 15s offset in timings. + # Allow a 5s offset in timings. # if raise_time - if raise_time <= (reception_time + 15) + if raise_time <= (reception_time + 5) alert_db.raised_at = raise_time else alert_db.will_raise_at = raise_time @@ -327,19 +366,24 @@ module Mauve end if clear_time - if clear_time <= (reception_time + 15) + if clear_time <= (reception_time + 5) alert_db.cleared_at = clear_time else alert_db.will_clear_at = clear_time end end - # re-raise + # + # Re-raise if raised_at and cleared_at are set. + # if alert_db.cleared_at && alert_db.raised_at && alert_db.cleared_at < alert_db.raised_at alert_db.cleared_at = nil end - if pre_cleared && alert_db.raised? + # + # + # + if (pre_raised or pre_cleared) && alert_db.raised? alert_db.update_type = :raised elsif pre_raised && alert_db.cleared? alert_db.update_type = :cleared @@ -355,8 +399,8 @@ module Mauve # These updates happen but do not sent the alert back to the # notification system. # - alert_db.importance = alert.importance if alert.importance != 0 - + alert_db.importance = alert.importance if alert.importance != 0 + # FIXME: this logic ought to be clearer as it may get more complicated # if alert_db.update_type @@ -369,6 +413,8 @@ module Mauve alert_db.update_type = :changed end + logger.debug "Saving #{alert_db}" + if !alert_db.save if alert_db.errors.respond_to?("full_messages") msg = alert_db.errors.full_messages @@ -394,7 +440,9 @@ module Mauve alerts_updated << alert_db end end - + + logger.debug "Got #{alerts_updated.length} alerts to notify about" + AlertGroup.notify(alerts_updated) end diff --git a/lib/mauve/alert_group.rb b/lib/mauve/alert_group.rb index d8156fa..288b263 100644 --- a/lib/mauve/alert_group.rb +++ b/lib/mauve/alert_group.rb @@ -29,11 +29,14 @@ module Mauve logger.warn "no groups found for #{alert.id}" if groups.empty? # - # Notify each group. + # Notify just the group that thinks this alert is the most urgent. # - groups.each do |grp| - logger.info("notifying group #{groups[0]} of AlertID.#{alert.id}.") - grp.notify(alert) + %w(urgent normal low).each do |lvl| + this_group = groups.find{|grp| grp.level.to_s == lvl} + next if this_group.nil? + logger.info("notifying group #{this_group} of AlertID.#{alert.id} (matching #{lvl})") + this_group.notify(alert) + break end end end diff --git a/lib/mauve/auth_bytemark.rb b/lib/mauve/auth_bytemark.rb index 7419d10..9e0a6d1 100644 --- a/lib/mauve/auth_bytemark.rb +++ b/lib/mauve/auth_bytemark.rb @@ -3,7 +3,7 @@ require 'sha1' require 'xmlrpc/client' require 'timeout' -class AuthSourceBytemark +class AuthBytemark def initialize (srv='auth.bytemark.co.uk', port=443) raise ArgumentError.new("Server must be a String, not a #{srv.class}") if String != srv.class @@ -11,6 +11,7 @@ class AuthSourceBytemark @srv = srv @port = port @timeout = 7 + @logger = Log4r::Logger.new(self.class.to_s) end ## Not really needed. @@ -33,15 +34,16 @@ class AuthSourceBytemark raise ArgumentError.new("Login must be a string, not a #{login.class}") if String != login.class raise ArgumentError.new("Password must be a string, not a #{password.class}") if String != password.class raise ArgumentError.new("Login or/and password is/are empty.") if login.empty? || password.empty? + client = XMLRPC::Client.new(@srv,"/",@port,nil,nil,nil,nil,true,@timeout).proxy("bytemark.auth") + begin challenge = client.getChallengeForUser(login) response = Digest::SHA1.new.update(challenge).update(password).hexdigest client.login(login, response) - rescue XMLRPC::FaultException => fault - return "Fault code is #{fault.faultCode} stating #{fault.faultString}" + rescue Exception => ex + return false end - return true end end diff --git a/lib/mauve/calendar_interface.rb b/lib/mauve/calendar_interface.rb index 08cfab3..ab2bc5b 100644 --- a/lib/mauve/calendar_interface.rb +++ b/lib/mauve/calendar_interface.rb @@ -27,7 +27,7 @@ module Mauve # @return [Array] A list of all the username on support. def self.get_users_on_support(url) result = get_URL(url) - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" logger.debug("Cheching who is on support: #{result}") return result end @@ -40,7 +40,7 @@ module Mauve # @param [String] usr User single sign on. # @return [Boolean] True if on support, false otherwise. def self.is_user_on_support?(url, usr) - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" list = get_URL(url) if true == list.include?("nobody") logger.error("Nobody is on support thus alerts are ignored.") @@ -63,7 +63,7 @@ module Mauve return false if true == list.nil? or true == list.empty? pattern = /[\d]{4}-[\d]{2}-[\d]{2}\s[\d]{2}:[\d]{2}:[\d]{2}/ result = (list[0].match(pattern))? true : false - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" logger.debug("Cheching if #{usr} is on holiday: #{result}") return result end @@ -83,7 +83,7 @@ module Mauve # @retur [Array] An array of strings, each newline creates an new item. def self.get_URL (uri_str, limit = 11) - logger = Log4r::Logger.new "mauve::CalendarInterface" + logger = Log4r::Logger.new "Mauve::CalendarInterface" if 0 == limit logger.warn("HTTP redirect deeper than 11 on #{uri_str}.") diff --git a/lib/mauve/configuration.rb b/lib/mauve/configuration.rb index b11e1b5..4b7717d 100644 --- a/lib/mauve/configuration.rb +++ b/lib/mauve/configuration.rb @@ -212,6 +212,7 @@ module Mauve is_attribute "port" is_attribute "ip" is_attribute "document_root" + is_attribute "session_secret" def builder_setup @result = HTTPServer.instance @@ -322,9 +323,17 @@ module Mauve end def holiday_url (url) - @result.holiday_url = url + @result.holiday_url = url.to_s end - + + def email(e) + @result.email = e.to_s + end + + def xmpp(x) + @result.xmpp = x.to_s + end + def suppress_notifications_after(h) raise ArgumentError.new("notification_threshold must be specified as e.g. (10 => 1.minute)") unless h.kind_of?(Hash) && h.keys[0].kind_of?(Integer) && h.values[0].kind_of?(Integer) @@ -384,7 +393,6 @@ module Mauve # Create a new instance and adds it. def builder_setup(label) - pp label @result = PeopleList.new(label) end diff --git a/lib/mauve/http_server.rb b/lib/mauve/http_server.rb index 69b566b..4fd8b60 100644 --- a/lib/mauve/http_server.rb +++ b/lib/mauve/http_server.rb @@ -2,15 +2,15 @@ # # Bleuurrgggggh! Bleurrrrrgghh! # +require 'mauve/auth_bytemark' +require 'mauve/web_interface' +require 'mauve/mauve_thread' require 'digest/sha1' require 'log4r' require 'thin' require 'rack' require 'rack-flash' require 'rack/handler/webrick' -require 'mauve/auth_bytemark' -require 'mauve/web_interface' -require 'mauve/mauve_thread' ################################################################################ # @@ -87,19 +87,19 @@ module Mauve attr_accessor :session_secret # not used yet def initialize - @port = 32761 + @port = 1288 @ip = "127.0.0.1" - @document_root = "." - @session_secret = rand(2**100).to_s + @document_root = "/usr/share/mauvealert" + @session_secret = "%x" % rand(2**100) end def main_loop - @server = ::Thin::Server.new(@ip, @port, Rack::CommonLogger.new(Rack::Chunked.new(Rack::ContentLength.new(WebInterface.new)), RackErrorsProxy.new(logger)), :signals => false) + @server = ::Thin::Server.new(@ip, @port, Rack::Session::Cookie.new(WebInterface.new, {:key => "mauvealert", :secret => @session_secret, :expire_after => 691200}), :signals => false) @server.start end def stop - @server.stop + @server.stop if @server super end end diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb index 2220211..02bf6fd 100644 --- a/lib/mauve/notification.rb +++ b/lib/mauve/notification.rb @@ -31,7 +31,7 @@ module Mauve @time = time @alert = alert @during = during || Proc.new { true } - @logger = Log4r::Logger.new "mauve::DuringRunner" + @logger = Log4r::Logger.new "Mauve::DuringRunner" end def now? @@ -137,6 +137,11 @@ module Mauve # def alert_changed(alert) + if people.nil? or people.empty? + logger.warn "No people found in for notification #{list}" + return + end + # Should we notificy at all? is_relevant = DuringRunner.new(MauveTime.now, alert, &during).now? diff --git a/lib/mauve/notifier.rb b/lib/mauve/notifier.rb index e0692f6..0127b6b 100644 --- a/lib/mauve/notifier.rb +++ b/lib/mauve/notifier.rb @@ -40,11 +40,38 @@ module Mauve def start super - Configuration.current.notification_methods['xmpp'].connect if Configuration.current.notification_methods['xmpp'] + if Configuration.current.notification_methods['xmpp'] + # + # Connect to XMPP server + # + xmpp = Configuration.current.notification_methods['xmpp'] + xmpp.connect + + Configuration.current.people.each do |username, person| + # + # Ignore people without XMPP stanzas. + # + next unless person.xmpp + + # + # For each JID, either ensure they're on our roster, or that we're in + # that chat room. + # + jid = if xmpp.is_muc?(person.xmpp) + xmpp.join_muc(person.xmpp) + else + xmpp.ensure_roster_and_subscription!(person.xmpp) + end + + Configuration.current.people[username].xmpp = jid unless jid.nil? + end + end end def stop - Configuration.current.notification_methods['xmpp'].close + if Configuration.current.notification_methods['xmpp'] + Configuration.current.notification_methods['xmpp'].close + end super end diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb index 2c14a54..f3b9a0f 100644 --- a/lib/mauve/notifiers/email.rb +++ b/lib/mauve/notifiers/email.rb @@ -38,6 +38,11 @@ module Mauve @suppressed_changed = nil end + def logger + @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") + + end + def send_alert(destination, alert, all_alerts, conditions = nil) message = prepare_message(destination, alert, all_alerts, conditions) args = [@server, @port] @@ -46,14 +51,11 @@ module Mauve Net::SMTP.start(*args) do |smtp| smtp.send_message(message, @from, destination) end - rescue Errno::ECONNREFUSED => e - @logger = Log4r::Logger.new "mauve::email_send_alert" - @logger.error("#{e.class}: #{e.message} raised. " + - "args = #{args.inspect} " - ) - raise e - rescue => e - raise e + true + rescue StandardError => ex + logger.error "SMTP failure: #{ex.to_s}" + logger.debug ex.backtrace.join("\n") + false end end @@ -98,8 +100,8 @@ module Mauve # FIXME: include alert.detail as multipart mime ##Thread.abort_on_exception = true m.body += "\n" + '-'*10 + " This is the detail field " + '-'*44 + "\n\n" - m.body += alert.get_details() - m.body += alert.get_details_plain_text() + m.body += alert.detail.to_s +#' m.body += alert.get_details_plain_text() m.body += "\n" + '-'*80 + "\n\n" if @suppressed_changed == true diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb index 18df6b2..fbc9640 100644 --- a/lib/mauve/notifiers/xmpp.rb +++ b/lib/mauve/notifiers/xmpp.rb @@ -1,68 +1,106 @@ require 'log4r' require 'xmpp4r' -require 'xmpp4r/xhtml' require 'xmpp4r/roster' -require 'xmpp4r/muc/helper/simplemucclient' +require 'xmpp4r/muc' +# require 'xmpp4r/xhtml' +# require 'xmpp4r/discovery/helper/helper' require 'mauve/notifiers/debug' -#Jabber::debug = true -module Mauve - module Notifiers - module Xmpp - - class CountingMUCClient < Jabber::MUC::SimpleMUCClient - attr_reader :participants - def initialize(*a) - super(*a) - @participants = 0 - self.on_join { @participants += 1 } - self.on_leave { @participants -= 1 } - end +# +# A couple of monkey patches to fix up all this nonsense. +# +module Jabber + class Stream + def close + # + # Just close + # + close! + end + def close! + 10.times do + pr = 0 + @tbcbmutex.synchronize { pr = @processing } + break if pr = 0 + Thread::pass if pr > 0 + sleep 1 end + + # Order Matters here! If this method is called from within + # @parser_thread then killing @parser_thread first would + # mean the other parts of the method fail to execute. + # That would be bad. So kill parser_thread last + @tbcbmutex.synchronize { @processing = 0 } + @fd.close if @fd and !@fd.closed? + @status = DISCONNECTED + stop + end + end +end + + + + +module Mauve + module Notifiers + module Xmpp +# class CountingMUCClient < Jabber::MUC::SimpleMUCClient +# +# attr_reader :participants +# +# def initialize(*a) +# super(*a) +# @participants = 0 +# self.on_join { @participants += 1 } +# self.on_leave { @participants -= 1 } +# end +# +# end +# class Default include Jabber - + # Atrtribute. attr_reader :name # Atrtribute. attr_accessor :jid, :password - # Atrtribute. - attr_accessor :initial_jid - - # Atrtribute. - attr_accessor :initial_messages - def initialize(name) + Jabber::logger = self.logger + #Jabber::debug = true + #Jabber::warnings = true + @name = name @mucs = {} @roster = nil + @closing = false + end def logger - @logger ||= Log4r::Logger.new self.class.to_s + # Give the logger a sane name + @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") + end + + def jid=(jid) + @jid = JID.new(jid) end + + def connect + logger.info "Jabber starting connection to #{@jid}" - def reconnect - if @client - begin - logger.debug "Jabber closing old client connection" - @client.close - @client = nil - @roster = nil - rescue Exception => ex - logger.error "#{ex} when reconnecting" - end - end + # Make sure we're disconnected. + self.close if @client.is_a?(Client) + + @client = Client.new(@jid) - logger.debug "Jabber starting connection to #{@jid}" - @client = Client.new(JID::new(@jid)) + @closing = false @client.connect @client.auth_nonsasl(@password, false) @roster = Roster::Helper.new(@client) @@ -70,11 +108,17 @@ module Mauve # Unconditionally accept all roster add requests, and respond with a # roster add + subscription request of our own if we're not subscribed # already - @roster.add_subscription_request_callback do |ri, stanza| + @roster.add_subscription_request_callback do |ri, presence| Thread.new do - logger.debug("Accepting subscription request from #{stanza.from}") - @roster.accept_subscription(stanza.from) - ensure_roster_and_subscription!(stanza.from) + logger.debug "Known? #{is_known_contact?(presence.from).inspect}" + if is_known_contact?(presence.from) + logger.info("Accepting subscription request from #{presence.from}") + @roster.accept_subscription(presence.from) + ensure_roster_and_subscription!(presence.from) + else + logger.info("Declining subscription request from #{presence.from}") + @roster.decline_subscription(presence.from) + end end.join end @@ -85,37 +129,40 @@ module Mauve @roster.wait_for_roster logger.debug "Jabber authenticated, setting presence" - @client.send(Presence.new.set_type(:available)) - - @mucs = {} - - logger.debug "Jabber is ready in theory" - end - - def reconnect_and_retry_on_error - @already_reconnected = false - begin - yield - rescue StandardError => ex - logger.error "#{ex} during notification\n" - logger.debug ex.backtrace - if !@already_reconnected - reconnect - @already_reconnected = true - retry - else - raise ex + @client.send(Presence.new(nil, "Woo!").set_type(nil)) + + @client.on_exception do |ex, stream, where| + # + # The XMPP4R exception clauses in Stream all close the stream, so + # we just need to reconnect. + # + unless ex.nil? or @closing + logger.warn(["Caught",ex.class,ex.to_s,"during XMPP",where].join(" ")) + connect + @mucs.each do |jid, muc| + @mucs.delete(jid) + join_muc(jid) + end end end end - def connect - self.reconnect_and_retry_on_error { self.send_msg(@initial_jid, "Hello!") } + # + # Kills the processor thread + # + def stop + @client.stop end def close - self.send_msg(@initial_jid, "Goodbye!") - @client.close + @closing = true + if @client and @client.is_connected? + @mucs.each do |jid, muc| + muc.exit("Goodbye!") if muc.active? + end + @client.send(Presence.new(nil, "Goodbye!").set_type(:unavailable)) + @client.close! + end end # Takes an alert and converts it into a message. @@ -144,97 +191,232 @@ module Mauve # or more of the choices - see +check_jid_has_presence+ for options. def send_alert(destination, alert, all_alerts, conditions = nil) - #message = Message.new(nil, alert.summary_two_lines.join("\n")) - message = Message.new(nil, convert_alert_to_message(alert)) - + destination_jid = JID.new(destination) + if conditions @suppressed_changed = conditions[:suppressed_changed] end - # MUC JIDs are prefixed with muc: - we need to strip this out. - destination_is_muc, dest_jid = self.is_muc?(destination) - - begin - xhtml = XHTML::HTML.new("

" + - convert_alert_to_message(alert)+ -# alert.summary_three_lines.join("
") + - #alert.summary_two_lines.join("
") + - "

") - message.add_element(xhtml) - rescue REXML::ParseException => ex - logger.warn("Can't send XMPP alert as valid XHTML-IM, falling back to plaintext") - logger.debug(ex) - end - - logger.debug "Jabber sending #{message} to #{destination}" - reconnect unless @client - - ensure_roster_and_subscription!(dest_jid) unless destination_is_muc - - if conditions && !check_alert_conditions(dest_jid, conditions) - logger.debug("Alert conditions not met, not sending XMPP alert to #{jid}") + if conditions && !check_alert_conditions(destination_jid, conditions) + logger.info("Alert conditions not met, not sending XMPP alert to #{destination_jid}") return false end - if destination_is_muc - if !@mucs[dest_jid] - @mucs[dest_jid] = CountingMUCClient.new(@client) - @mucs[dest_jid].join(JID.new(dest_jid)) - end - reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } - else - message.to = dest_jid - reconnect_and_retry_on_error { @client.send(message) ; true } - end + send_message(destination_jid, convert_alert_to_message(alert)) end # Sends a message to the destionation. # - # @param [String] destionation The (full) JID to send to. + # @param [String] destination The (full) JID to send to. # @param [String] msg The (formatted) message to send. # @return [NIL] nada. - def send_msg(destination, msg) - reconnect unless @client - message = Message.new(nil, msg) - destination_is_muc, dest_jid = self.is_muc?(destination) - if destination_is_muc - if !@mucs[dest_jid] - @mucs[dest_jid] = CountingMUCClient.new(@client) - @mucs[dest_jid].join(JID.new(dest_jid)) + def send_message(jid, msg) + jid = JID.new(jid) unless jid.is_a?(JID) + + message = Message.new(jid) + + #if msg.is_a?(XHTML::HTML) + # message.add_element(msg) + #else + message.body = msg + #end + + if is_muc?(jid) + jid = join_muc(jid.strip) + muc = @mucs[jid] + + if muc + message.to = muc.jid + muc.send(message) + true + else + logger.warn "Failed to join MUC #{jid} when trying to send a message" + false end - reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } else - message.to = dest_jid - reconnect_and_retry_on_error { @client.send(message) ; true } + # + # We aren't interested in sending things to people who aren't online. + # + ensure_roster_and_subscription!(jid) + + if check_jid_has_presence(jid) + # + # We set the chat type to chat + # + message.type = :chat + message.to = jid + @client.send(message) + true + else + false + end end - return nil end - protected + # + # Joins a chat, and returns the stripped JID of the chat joined. + # + def join_muc(jid, password=nil) + if jid.is_a?(String) + jid = JID.new($1) if jid =~ /^muc:(.*)/ + end + + unless jid.is_a?(JID) + logger.warn "I don't think #{jid} is a MUC" + return + end + + jid.resource = @client.jid.resource if jid.resource.to_s.empty? + + if !@mucs[jid.strip] + + logger.info("Adding new MUC client for #{jid}") + + @mucs[jid.strip] = Jabber::MUC::MUCClient.new(@client) + + # Add some callbacks + @mucs[jid.strip].add_message_callback do |m| + receive_message(m) + end + + @mucs[jid.strip].add_private_message_callback do |m| + receive_message(m) + end + + end + + if !@mucs[jid.strip].active? + logger.info("Joining #{jid}") + # + # Make sure we have a resource. + # + @mucs[jid.strip].join(jid, password) - # Checks whether the destination JID is a MUC. - # Returns [true/false, destination] - def is_muc?(destination) - if /^muc:(.*)/.match(destination) - [true, $1] else - [false, destination] + logger.debug("Already joined #{jid}.") end - end + + # + # Return the JID object + # + jid.strip + end + # + # Checks whether the destination JID is a MUC. + # + def is_muc?(jid) + (jid.is_a?(JID) and @mucs.keys.include?(jid.strip)) or + (jid.is_a?(String) and jid =~ /^muc:(.*)/) + + # + # It would be nice to use service discovery to determin this, but it + # turns out that it is shite in xmpp4r. It doesn't return straight + # away with an answer, making it a bit useless. Some sort of weird + # threading issue, I think. + # + # begin + # logger.warn caller.join("\n") + # cl = Discovery::Helper.new(@client) + # res = cl.get_info_for(jid.strip) + # @client.wait + # logger.warn "hello #{res.inspect}" + # res.is_a?(Discovery::IqQueryDiscoInfo) and res.identity.category == :conference + # rescue Jabber::ServerError => ex + # false + # end + end + + # # Checks to see if the JID is in our roster, and whether we are # subscribed to it or not. Will add to the roster and subscribe as # is necessary to ensure both are true. + # def ensure_roster_and_subscription!(jid) - jid = JID.new(jid) - ri = @roster.find(jid)[jid] - if ri.nil? - @roster.add(jid, nil, true) - else - ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) - end - rescue Exception => ex + return jid if is_muc?(jid) + + jid = JID.new(jid) unless jid.is_a?(JID) + + ri = @roster.find(jid).values.first + @roster.add(jid, nil, true) if ri.nil? + + ri = @roster.find(jid).values.first + ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) + ri.jid + rescue StandardError => ex logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}") + nil + end + + protected + + def receive_message(msg) + # We only want to hear messages from known contacts. + unless is_known_contact?(msg.from) + # ignore message + logger.info "Ignoring message from unknown contact #{msg.from}" + return nil + end + + case msg.type + when :error + receive_error_message(msg) + when :groupchat + receive_groupchat_message(msg) + else + receive_normal_message(msg) + end + end + + def receive_error_message(msg) + logger.warn("Caught XMPP error #{msg}") + nil + end + + def receive_normal_message(msg) + # + # Treat invites specially + # + if msg.x("jabber:x:conference") + # + # recieved an invite. Need to mangle the jid. + # + jid =JID.new(msg.x("jabber:x:conference").attribute("jid")) + # jid.resource = @client.jid.resource + logger.info "Received an invite to #{jid}" + unless join_muc(jid) + logger.warn "Failed to join MUC #{jid} following invitation" + return nil + end + elsif msg.body + # + # Received a message with a body. + # + jid = msg.from + end + + # + # I don't have time to talk to myself! + # + if jid and jid.strip != @client.jid.strip + txt = File.executable?('/usr/games/fortune') ? `/usr/games/fortune -s -n 60`.chomp : "I'd love to stay and chat, but I'm really too busy." + send_message(jid, txt) + end + end + + def receive_groupchat_message(msg) + # + # We only want group chat messages from MUCs we're already joined to, + # that we've not sent ourselves, that are not historical, and that + # match our resource or node in the body. + # + if @mucs[msg.from.strip].is_a?(MUC::MUCClient) and + msg.from != @mucs[msg.from.strip].jid and + msg.x("jabber:x:delay") == nil and + (msg.body =~ /\b#{Regexp.escape(@client.jid.resource)}\b/i or + msg.body =~ /\b#{Regexp.escape(@client.jid.node)}\b/i) + receive_normal_message(msg) + end end def check_alert_conditions(destination, conditions) @@ -264,8 +446,11 @@ module Mauve # Returns true if at least one of the presence specifiers for the jid # is met, false otherwise. Note that if the alerter can't see the alertee's # presence, only 'unknown' will match - generally, you'll want [:online, :unknown] - def check_jid_has_presence(jid, presence_or_presences) - return true if jid.match(/^muc:/) + def check_jid_has_presence(jid, presence_or_presences = [:online, :unknown]) + jid = JID.new(jid) unless jid.is_a?(JID) + + + return true if is_muc?(jid) reconnect unless @client @@ -291,15 +476,16 @@ module Mauve end results.include?(true) end - - end - # - # TODO parse message and ack as needed..? The trick is here to - # understand what the person sending the message wants. Could be - # difficult. - def receive_message(message) - @logger.debug "Received message from #{message.from}.. Ignoring for now." + def is_known_contact?(jid) + jid = JID.new(jid) unless jid.is_a?(JID) + + Configuration.current.people.any? do |username, person| + next unless person.xmpp.is_a?(JID) + person.xmpp.strip == jid.strip + end + end + end end end diff --git a/lib/mauve/people_list.rb b/lib/mauve/people_list.rb index 2e4c737..23e0c1e 100644 --- a/lib/mauve/people_list.rb +++ b/lib/mauve/people_list.rb @@ -21,19 +21,21 @@ module Mauve alias username label def list - self[:list] + self[:list] || [] end # # Set up the logger def logger - @logger ||= Log4r::Logger.new self.class + @logger ||= Log4r::Logger.new self.class.to_s end # # Return the array of people # def people + logger.warn "No-one found in the people list for #{self.label}" if self.list.empty? + list.collect do |name| Configuration.current.people.has_key?(name) ? Configuration.current.people[name] : nil end.reject{|person| person.nil?} diff --git a/lib/mauve/person.rb b/lib/mauve/person.rb index da3aa13..42b6baf 100644 --- a/lib/mauve/person.rb +++ b/lib/mauve/person.rb @@ -3,7 +3,7 @@ require 'timeout' require 'log4r' module Mauve - class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low) + class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low, :email, :xmpp, :sms) attr_reader :notification_thresholds @@ -24,26 +24,50 @@ module Mauve # class NotificationCaller - def initialize(alert, other_alerts, notification_methods, base_conditions={}) - logger = Log4r::Logger.new "mauve::NotificationCaller" + def initialize(person, alert, other_alerts, notification_methods, base_conditions={}) + @person = person @alert = alert @other_alerts = other_alerts @notification_methods = notification_methods @base_conditions = base_conditions end - def method_missing(name, destination, *args) - conditions = @base_conditions.merge(args[0] ? args[0] : {}) - notification_method = @notification_methods[name.to_s] + def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end + + # + # This method makes sure things liek + # + # xmpp + # works + # + def method_missing(name, *args) + # + # Work out the destination + # + if args.first.is_a?(String) + destination = args.pop + else + destination = @person.__send__(name) + end - unless notification_method - raise NoMethodError.new("#{name} not defined as a notification method") + if args.first.is_a?(Array) + conditions = @base_conditions.merge(args[0]) end + + notification_method = Configuration.current.notification_methods[name.to_s] + + raise NoMethodError.new("#{name} not defined as a notification method") unless notification_method + # Methods are expected to return true or false so the user can chain # them together with || as fallbacks. So we have to catch exceptions # and turn them into false. # - notification_method.send_alert(destination, @alert, @other_alerts, conditions) + res = notification_method.send_alert(destination, @alert, @other_alerts, conditions) + # + # Log the result + logger.debug "Notification " + (res ? "succeeded" : "failed" ) + " for #{@person.username} using notifier '#{name}' to '#{destination}'" + + res end end @@ -174,6 +198,7 @@ module Mauve return if suppressed? or this_alert_suppressed result = NotificationCaller.new( + self, alert, current_alerts, Configuration.current.notification_methods, @@ -197,7 +222,7 @@ module Mauve # Returns the subset of current alerts that are relevant to this Person. # def current_alerts - Alert.all_current.select do |alert| + Alert.all_raised.select do |alert| my_last_update = AlertChanged.first(:person => username, :alert_id => alert.id) my_last_update && my_last_update.update_type != :cleared end diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb index 30536bb..ac24da4 100644 --- a/lib/mauve/server.rb +++ b/lib/mauve/server.rb @@ -12,7 +12,6 @@ require 'mauve/processor' require 'mauve/http_server' require 'log4r' - module Mauve class Server @@ -70,7 +69,7 @@ module Mauve # DataMapper.setup(:default, @config[:database]) - DataObjects::Sqlite3.logger = Log4r::Logger.new("Mauve::DataMapper") + # DataObjects::Sqlite3.logger = Log4r::Logger.new("Mauve::DataMapper") # # Update any tables. @@ -82,7 +81,7 @@ module Mauve # # Work out when the server was last stopped # - @stopped_at = self.last_heartbeat + # topped_at = self.last_heartbeat end def last_heartbeat @@ -91,7 +90,7 @@ module Mauve # [ Alert.last(:order => :updated_at.asc), AlertChanged.last(:order => :updated_at.asc) ]. - reject{|a| a.nil?}. + reject{|a| a.nil? or a.updated_at.nil? }. collect{|a| a.updated_at.to_time}. sort. last @@ -108,26 +107,61 @@ module Mauve def stop @stop = true + thread_list = Thread.list + + thread_list.delete(Thread.current) + THREAD_CLASSES.reverse.each do |klass| - klass.instance.stop + thread_list.delete(klass.instance) + klass.instance.stop unless klass.instance.nil? end - + + thread_list.each do |t| + t.exit + end + @logger.info("All threads stopped") end def run loop do + thread_list = Thread.list + + thread_list.delete(Thread.current) + THREAD_CLASSES.each do |klass| + thread_list.delete(klass.instance) + next if @frozen or @stop - + unless klass.instance.alive? - # ugh something has died. - klass.instance.join + # ugh something has died. + # + begin + klass.instance.join + rescue StandardError => ex + @logger.warn "Caught #{ex.to_s} whilst checking #{klass} thread" + @logger.debug ex.backtrace.join("\n") + end + # + # Start the stuff. klass.instance.start unless @stop end end + thread_list.each do |t| + next unless t.alive? + begin + t.join + rescue StandardError => ex + @logger.fatal "Caught #{ex.to_s} whilst checking threads" + @logger.debug ex.backtrace.join("\n") + self.stop + break + end + end + break if @stop sleep 1 diff --git a/lib/mauve/source_list.rb b/lib/mauve/source_list.rb index 4ffef15..23f5ae1 100644 --- a/lib/mauve/source_list.rb +++ b/lib/mauve/source_list.rb @@ -27,7 +27,7 @@ module Mauve ## Default contructor. def initialize () - @logger = Log4r::Logger.new "mauve::SourceList" + @logger = Log4r::Logger.new "Mauve::SourceList" @hash = Hash.new @http_head = Regexp.compile(/^http[s]?:\/\//) @http_tail = Regexp.compile(/\/.*$/) diff --git a/lib/mauve/timer.rb b/lib/mauve/timer.rb index 5355dcc..60b541c 100644 --- a/lib/mauve/timer.rb +++ b/lib/mauve/timer.rb @@ -21,7 +21,6 @@ module Mauve end def main_loop - @logger.debug "hello" # # Get the next alert. # @@ -73,7 +72,12 @@ module Mauve # # This is a rate-limiting step for alerts. # - Kernel.sleep 0.2 + Kernel.sleep 0.1 + # + # Not sure if this is needed or not. But the timer thread seems to + # freeze here, apparently stuck on a select() statement. + # + Thread.pass end return if self.should_stop? or next_to_notify.nil? diff --git a/lib/mauve/udp_server.rb b/lib/mauve/udp_server.rb index a570e8a..a873e77 100644 --- a/lib/mauve/udp_server.rb +++ b/lib/mauve/udp_server.rb @@ -68,11 +68,13 @@ module Mauve # # TODO: why is/isn't this non-block? # + i = 0 begin - # packet = @socket.recvfrom_nonblock(65535) - packet = @socket.recvfrom(65535) + packet = @socket.recvfrom_nonblock(65535) +# packet = @socket.recvfrom(65535) received_at = MauveTime.now - rescue Errno::EAGAIN, Errno::EWOULDBLOCK + rescue Errno::EAGAIN, Errno::EWOULDBLOCK => ex + puts "#{i += 1} + #{ex}" IO.select([@socket]) retry unless self.should_stop? end @@ -99,7 +101,7 @@ module Mauve # # Triggers loop to close socket. # - UDPSocket.open.send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.closed? + UDPSocket.open.send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.nil? or @socket.closed? super end diff --git a/lib/mauve/web_interface.rb b/lib/mauve/web_interface.rb index 4569cb6..210f88a 100644 --- a/lib/mauve/web_interface.rb +++ b/lib/mauve/web_interface.rb @@ -1,7 +1,11 @@ # encoding: UTF-8 +require 'haml' +require 'redcloth' + +require 'sinatra/tilt' require 'sinatra/base' require 'sinatra-partials' -require 'haml' + require 'rack' require 'rack-flash' @@ -16,15 +20,18 @@ module Mauve class PleaseAuthenticate < Exception; end - use Rack::Session::Cookie, :expire_after => 604800 # 7 days in seconds + use Rack::CommonLogger + use Rack::Chunked + use Rack::ContentLength + use Rack::Flash - enable :sessions +# Tilt.register :textile, RedClothTemplate - use Rack::Flash - - set :root, "/usr/share/mauve" - set :views, "#{root}/views" - set :public, "#{root}/static" + + # Ugh.. hacky way to dynamically configure the document root. + set :root, Proc.new{ HTTPServer.instance.document_root } + set :views, Proc.new{ root && File.join(root, 'views') } + set :public, Proc.new{ root && File.join(root, 'static') } set :static, true set :show_exceptions, true @@ -39,12 +46,43 @@ module Mauve ######################################################################## before do - @person = Configuration.current.people[session['username']] @title = "Mauve alert panel" + @person = nil + # + # Make sure we're authenticated. + # + + if session.has_key?('username') and Configuration.current.people.has_key?(session['username'].to_s) + # + # Phew, we're authenticated + # + @person = Configuration.current.people[session['username']] + + # + # A bit wasteful maybe..? + # + @alerts_raised = Alert.all_raised + @alerts_cleared = Alert.all_cleared + @alerts_ackd = Alert.all_acknowledged + @group_by = "subject" + else + # Uh-oh.. Intruder alert! + # + ok_urls = %w(/ /login /logout) + + unless ok_urls.include?(request.path_info) + flash['error'] = "You must be logged in to access that page." + redirect "/login?next_page=#{request.path_info}" + end + end end get '/' do - redirect '/alerts' + if @person.nil? + redirect '/login' + else + redirect '/alerts' + end end ######################################################################## @@ -53,36 +91,69 @@ module Mauve # # The password can be either the SSO or a local one defined # in the configuration file. - # + + get '/login' do + if @person + redirect '/' + else + @next_page = params[:next_page] || '/' + haml :login + end + end + post '/login' do usr = params['username'] pwd = params['password'] - ret_sso = helper_auth_SSO(usr, pwd) - ret_loc = helper_auth_local(usr, pwd) - if "success" == ret_sso or "success" == ret_loc + next_page = params['next_page'] + # + # Make sure we don't magically logout automatically :) + # + next_page = '/' if next_page == '/logout' + + if auth_helper(usr, pwd) session['username'] = usr + redirect next_page else - flash['error'] =<<__MSG -

-ACCESS DENIED
-#{ret_sso}
-#{ret_loc}
-__MSG + flash['error'] = "Access denied." end - redirect '/alerts' end get '/logout' do session.delete('username') - redirect '/alerts' + redirect '/login' end get '/alerts' do - #now = MauveTime.now.to_f - please_authenticate() - find_active_alerts() - #pp MauveTime.now.to_f - now - haml(:alerts2) + redirect '/alerts/raised' + end + + get '/alerts/:alert_type' do + redirect "/alerts/#{params[:alert_type]}/subject" + end + + get '/alerts/:alert_type/:group_by' do + if %w(raised cleared acknowledged).include?(params[:alert_type]) + @alert_type = params[:alert_type] + else + @alert_type = "raised" + end + + if %w(subject source summary id alert_id level).include?(params[:group_by]) + @group_by = params[:group_by] + else + @group_by = "subject" + end + + case @alert_type + when "raised" + @grouped_alerts = group_by(@alerts_raised, @group_by) + when "cleared" + @grouped_alerts = group_by(@alerts_cleared, @group_by) + when "acknowledged" + @grouped_alerts = group_by(@alerts_ackd, @group_by) + end + + haml(:alerts) end get '/_alert_summary' do @@ -98,22 +169,20 @@ __MSG partial("head") end - get '/alert/:id/detail' do - please_authenticate - - content_type("text/html") # I think - Alert.get(params[:id]).detail + get '/alert/:id/_detail' do + content_type "text/html" + alert = Alert.get(params[:id]) + + haml :_detail, :locals => { :alert => alert } unless alert.nil? end get '/alert/:id' do - please_authenticate find_active_alerts @alert = Alert.get(params['id']) haml :alert end post '/alert/:id/acknowledge' do - please_authenticate alert = Alert.get(params[:id]) if alert.acknowledged? @@ -128,7 +197,6 @@ __MSG # Note that :until must be in seconds. post '/alert/acknowledge/:id/:until' do #now = MauveTime.now.to_f - please_authenticate alert = Alert.get(params[:id]) alert.acknowledge!(@person, params[:until].to_i()) @@ -140,7 +208,6 @@ __MSG post '/alert/:id/raise' do #now = MauveTime.now.to_f - please_authenticate alert = Alert.get(params[:id]) alert.raise! @@ -150,7 +217,6 @@ __MSG end post '/alert/:id/clear' do - please_authenticate alert = Alert.get(params[:id]) alert.clear! @@ -159,7 +225,6 @@ __MSG end post '/alert/:id/toggleDetailView' do - please_authenticate alert = Alert.get(params[:id]) if nil != alert @@ -171,8 +236,6 @@ __MSG end post '/alert/fold/:subject' do - please_authenticate - session[:display_folding][params[:subject]] = (true == session[:display_folding][params[:subject]])? false : true content_type("application/json") 'all is good'.to_json @@ -181,7 +244,6 @@ __MSG ######################################################################## get '/preferences' do - please_authenticate find_active_alerts haml :preferences end @@ -189,7 +251,6 @@ __MSG ######################################################################## get '/events' do - please_authenticate find_active_alerts find_recent_alerts haml :events @@ -199,11 +260,21 @@ __MSG helpers do include Sinatra::Partials - - def please_authenticate - raise PleaseAuthenticate.new unless @person + + def group_by(things, meth) + return {} if things.empty? + + raise ArgumentError.new "#{things.first.class} does not respond to #{meth}" unless things.first.respond_to?(meth) + + results = Hash.new{|h,k| h[k] = Array.new} + + things.each do |thing| + results[thing.__send__(meth)] << thing + end + + results end - + def find_active_alerts # FIXME: make sure alerts only appear once some better way @@ -287,25 +358,45 @@ __MSG ## Test for authentication with SSO. # - def helper_auth_SSO (usr, pwd) - auth = AuthSourceBytemark.new() - begin - return "success" if true == auth.authenticate(usr,pwd) - return "SSO did not regcognise your login/password combination." - rescue ArgumentError => ex - return "SSO argument error: #{ex.message}" - rescue => ex - return "SSO generic error: #{ex.message}" + def auth_helper (usr, pwd) + # First try Bytemark + # + auth = AuthBytemark.new() + result = begin + auth.authenticate(usr,pwd) + rescue Exception => ex + @logger.debug "Caught exception during Bytemark auth for #{usr} (#{ex.to_s})" + false end - end - ## Test for authentication with configuration file parameter. - # - def helper_auth_local (usr, pwd) - person = Configuration.current.people[params['username']] - return "I did not recognise your local login details." if !person - return "I did not recognise your local password." if Digest::SHA1.hexdigest(params['password']) != person.password - return "success" + if true == result + return true + else + @logger.debug "Bytemark authentication failed for #{usr}" + end + + # + # OK now try local auth + # + result = begin + if Configuration.current.people.has_key?(usr) + Digest::SHA1.hexdigest(params['password']) == Configuration.current.people[usr].password + end + rescue Exception => ex + @logger.debug "Caught exception during local auth for #{usr} (#{ex.to_s})" + false + end + + if true == result + return true + else + @logger.debug "Local authentication failed for #{usr}" + end + # + # Rate limit logins. + # + sleep 5 + false end end @@ -316,14 +407,13 @@ __MSG status 403 session[:display_alerts] = Hash.new() session[:display_folding] = Hash.new() - haml :please_authenticate end ######################################################################## # @see http://stackoverflow.com/questions/2239240/use-rackcommonlogger-in-sinatra def call(env) if true == @logger.nil? - @logger = Log4r::Logger.new("mauve::Rack") + @logger = Log4r::Logger.new("Mauve::Rack") end env['rack.errors'] = RackErrorsProxy.new(@logger) super(env) -- cgit v1.2.1