aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--debian/changelog8
-rw-r--r--lib/mauve/auth_bytemark.rb55
-rw-r--r--lib/mauve/authentication.rb125
-rw-r--r--lib/mauve/http_server.rb21
-rw-r--r--lib/mauve/notifiers/email.rb4
-rw-r--r--lib/mauve/pop3_server.rb315
-rw-r--r--lib/mauve/server.rb3
-rw-r--r--lib/mauve/version.rb2
-rw-r--r--lib/mauve/web_interface.rb50
9 files changed, 469 insertions, 114 deletions
diff --git a/debian/changelog b/debian/changelog
index dc1c59d..44c479e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+mauvealert (3.3.0) stable; urgency=low
+
+ * Added pop3 server
+ * Rejigged authentication
+ * Web interface improvements
+
+ -- Patrick J Cherry <patrick@bytemark.co.uk> Thu, 04 Aug 2011 14:26:24 +0100
+
mauvealert (3.2.1) stable; urgency=low
* Fixed up alert_group matching.
diff --git a/lib/mauve/auth_bytemark.rb b/lib/mauve/auth_bytemark.rb
deleted file mode 100644
index c0834e0..0000000
--- a/lib/mauve/auth_bytemark.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# encoding: UTF-8
-require 'sha1'
-require 'xmlrpc/client'
-require 'timeout'
-
-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
- raise ArgumentError.new("Port must be a Fixnum, not a #{port.class}") if Fixnum != port.class
- @srv = srv
- @port = port
- @timeout = 7
- @logger = Log4r::Logger.new(self.class.to_s)
- end
-
- ## Not really needed.
- def ping ()
- begin
- MauveTimeout.timeout(@timeout) do
- s = TCPSocket.open(@srv, @port)
- s.close()
- return true
- end
- rescue MauveTimeout::Error => ex
- return false
- rescue => ex
- return false
- end
- return false
- end
-
- def authenticate(login, password)
- 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)
- return true
- rescue XMLRPC::FaultException => fault
- Mauve::Server.instance.logger.warn "Fault code is #{fault.faultCode} stating #{fault.faultString}"
- return false
- rescue Exception => ex
- Mauve::Server.instance.logger.error "Caught #{ex.to_s} whilst trying to loging for #{login}"
- Mauve::Server.instance.logger.debug ex.backtrace.join("\n")
- return false
- end
- end
-
-end
diff --git a/lib/mauve/authentication.rb b/lib/mauve/authentication.rb
new file mode 100644
index 0000000..f01fb5e
--- /dev/null
+++ b/lib/mauve/authentication.rb
@@ -0,0 +1,125 @@
+# encoding: UTF-8
+require 'sha1'
+require 'xmlrpc/client'
+require 'timeout'
+
+module Mauve
+
+ class Authentication
+
+ ORDER = []
+
+ def authenticate(login, password)
+ 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?
+
+ return false unless Mauve::Configuration.current.people.has_key?(login)
+
+ false
+ end
+
+ def logger
+ self.class.logger
+ end
+
+ def self.logger
+ @logger ||= Log4r::Logger.new(self.to_s)
+ end
+
+ def self.authenticate(login, password)
+ result = false
+
+ ORDER.each do |klass|
+ auth = klass.new
+
+ result = begin
+ auth.authenticate(login, password)
+ rescue StandardError => ex
+ logger.error "#{ex.class}: #{ex.to_s} during #{auth.class} for #{login}"
+ logger.debug ex.backtrace.join("\n")
+ false
+ end
+
+ if true == result
+ logger.info "Authenticated #{login} using #{auth.class.to_s}"
+ break
+ end
+ end
+
+ unless true == result
+ logger.info "Authentication for #{login} failed"
+ # Rate limit
+ sleep 5
+ end
+
+ result
+ end
+
+ end
+
+
+ class AuthBytemark < Authentication
+
+ Mauve::Authentication::ORDER << self
+
+ #
+ # TODO: allow configuration of where the server is.
+ #
+ 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
+ raise ArgumentError.new("Port must be a Fixnum, not a #{port.class}") if Fixnum != port.class
+ @srv = srv
+ @port = port
+ @timeout = 7
+ end
+
+ ## Not really needed.
+ def ping ()
+ begin
+ MauveTimeout.timeout(@timeout) do
+ s = TCPSocket.open(@srv, @port)
+ s.close()
+ return true
+ end
+ rescue MauveTimeout::Error => ex
+ return false
+ rescue => ex
+ return false
+ end
+ return false
+ end
+
+ def authenticate(login, password)
+ super
+
+ 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)
+ return true
+ rescue XMLRPC::FaultException => fault
+ logger.warn "Authentication for #{login} failed: #{fault.faultCode}: #{fault.faultString}"
+ return false
+ rescue IOError => ex
+ logger.warn "#{ex.class} during auth for #{login} (#{ex.to_s})"
+ return false
+ end
+ end
+
+ end
+
+ class AuthLocal < Authentication
+
+ Mauve::Authentication::ORDER << self
+
+ def authenticate(login,password)
+ super
+ Digest::SHA1.hexdigest(password) == Mauve::Configuration.current.people[login].password
+ end
+
+ end
+
+end
diff --git a/lib/mauve/http_server.rb b/lib/mauve/http_server.rb
index d0ee29f..2b8d5cf 100644
--- a/lib/mauve/http_server.rb
+++ b/lib/mauve/http_server.rb
@@ -2,7 +2,6 @@
#
# Bleuurrgggggh! Bleurrrrrgghh!
#
-require 'mauve/auth_bytemark'
require 'mauve/web_interface'
require 'mauve/mauve_thread'
require 'digest/sha1'
@@ -136,11 +135,13 @@ module Mauve
end
def main_loop
- #
- # Sessions are kept for 8 days.
- #
- @server = ::Thin::Server.new(@ip, @port, Rack::Session::Cookie.new(WebInterface.new, {:key => "mauvealert", :secret => @session_secret, :expire_after => 691200}), :signals => false)
- @server.start
+ unless @server and @server.running?
+ #
+ # Sessions are kept for 8 days.
+ #
+ @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
end
def base_url
@@ -148,8 +149,14 @@ module Mauve
end
def stop
- @server.stop if @server
+ @server.stop if @server and @server.running?
super
end
+
+ def join
+ @server.stop! if @server and @server.running?
+ super
+ end
+
end
end
diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb
index a06d332..03384f7 100644
--- a/lib/mauve/notifiers/email.rb
+++ b/lib/mauve/notifiers/email.rb
@@ -51,8 +51,6 @@ module Mauve
end
end
- protected
-
def prepare_message(destination, alert, all_alerts, conditions = {})
was_suppressed = conditions[:was_suppressed] || false
is_suppressed = conditions[:is_suppressed] || false
@@ -75,7 +73,7 @@ module Mauve
m.header.to = destination
m.header.from = @from
- m.header.date = MauveTime.now
+ m.header.date = alert.updated_at.to_time || MauveTime.now
m.header['Content-Type'] = "multipart/alternative"
txt_template = File.join(File.dirname(__FILE__), "templates", "email.txt.erb")
diff --git a/lib/mauve/pop3_server.rb b/lib/mauve/pop3_server.rb
new file mode 100644
index 0000000..b83e839
--- /dev/null
+++ b/lib/mauve/pop3_server.rb
@@ -0,0 +1,315 @@
+require 'thin'
+require 'mauve/mauve_thread'
+require 'digest/sha1'
+
+module Mauve
+ #
+ # API to control the web server
+ #
+ class Pop3Server < MauveThread
+
+ include Singleton
+
+ attr_reader :port, :ip
+
+ def initialize
+ super
+ self.port = 1110
+ self.ip = "0.0.0.0"
+ end
+
+ def port=(pr)
+ raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0
+ @port = pr
+ end
+
+ def ip=(i)
+ raise ArgumentError, "ip must be a string" unless i.is_a?(String)
+ #
+ # Use ipaddr to sanitize our IP.
+ #
+ @ip = IPAddr.new(i)
+ end
+
+ def logger
+ @logger ||= Log4r::Logger.new(self.class.to_s)
+ end
+
+ def main_loop
+ unless @server and @server.running?
+ @server = Mauve::Pop3Backend.new(@ip.to_s, @port)
+ logger.info "Listening on #{@server.to_s}"
+ @server.start
+ end
+ end
+
+ def stop
+ @server.stop if @server and @server.running?
+ super
+ end
+
+ def join
+ @server.stop! if @server and @server.running?
+ super
+ end
+
+ end
+
+ class Pop3Backend < Thin::Backends::TcpServer
+
+ def logger
+ @logger ||= Log4r::Logger.new(self.class.to_s)
+ end
+
+ # Connect the server
+ def connect
+ @signature = EventMachine.start_server(@host, @port, Pop3Connection)
+ end
+
+ end
+
+
+ class Pop3Connection < EventMachine::Connection
+
+ attr_reader :user
+
+ CRLF = "\r\n"
+
+ def logger
+ @logger ||= Log4r::Logger.new(self.class.to_s)
+ end
+
+ def post_init
+ logger.info "New connection"
+ send_data "+OK #{self.class.to_s} started"
+ @state = :authorization
+ @user = nil
+ @messages = []
+ @level = nil
+ end
+
+ def permitted_commands(state=@state)
+ case @state
+ when :authorization
+ %w(QUIT USER PASS CAPA)
+ when :transaction
+ %w(QUIT STAT LIST RETR DELE NOOP RSET UIDL CAPA)
+ when :update
+ %w(QUIT)
+ end
+ end
+
+ def capabilities(state=@state)
+ case @state
+ when :transaction
+ %w(CAPA UIDL)
+ when :authorization
+ %w(CAPA UIDL USER)
+ else
+ []
+ end
+ end
+
+ def receive_data (data)
+ data.split(CRLF).each do |d|
+ break if error?
+
+ if d =~ Regexp.new('\A('+self.permitted_commands.join("|")+')\b')
+ case $1
+ when "QUIT"
+ do_process_quit data
+ when "USER"
+ do_process_user data
+ when "PASS"
+ do_process_pass data
+ when "STAT"
+ do_process_stat data
+ when "LIST"
+ do_process_list data
+ when "RETR"
+ do_process_retr data
+ when "DELE"
+ do_process_dele data
+ when "NOOP"
+ do_process_noop data
+ when "RSET"
+ do_process_rset data
+ when "CAPA"
+ do_process_capa data
+ when "UIDL"
+ do_process_uidl data
+ else
+ do_process_error data
+ end
+ else
+ do_process_error data
+ end
+ end
+ end
+
+ def send_data(d)
+ d += CRLF
+ super unless error?
+ end
+
+ def do_process_capa(a)
+ send_data (["+OK Capabilities follow:"] + self.capabilities + ["."]).join(CRLF)
+ end
+
+ def do_process_user(s)
+ allowed_levels = Mauve::AlertGroup::LEVELS.collect{|l| l.to_s}
+
+ if s =~ /\AUSER +(\w+)\+(#{allowed_levels.join("|")})/
+ # Allow alerts to be shown by level.
+ #
+ @user = $1
+ @level = $2
+ #
+ send_data "+OK Only going to show #{@level} alerts."
+
+ elsif s =~ /\AUSER +([\w]+)/
+ @user = $1
+
+ send_data "+OK"
+ else
+ send_data "-ERR Username not understood."
+ end
+ end
+
+ def do_process_pass(s)
+
+ if @user and s =~ /\APASS +(\S+)/
+ if Mauve::Authentication.authenticate(@user, $1)
+ @state = :transaction
+ send_data "+OK Welcome #{@user} (#{@level})."
+ else
+ send_data "-ERR Authentication failed."
+ end
+ else
+ send_data "-ERR USER comes first."
+ end
+ end
+
+ def do_process_error(a)
+ send_data "-ERR Unknown comand."
+ end
+
+ def do_process_noop(a)
+ send_data "+OK Thanks."
+ end
+
+ alias do_process_dele do_process_noop
+
+ def do_process_quit(a)
+ @state = :update
+
+ send_data "+OK bye."
+
+ close_connection_after_writing
+ end
+
+ def do_process_stat(a)
+ send_data "+OK #{self.messages.length} #{self.messages.inject(0){|s,m| s+= m[1].length}}"
+ end
+
+ def do_process_list(a)
+ d = []
+ if a =~ /\ALIST +(\d+)\b/
+ ind = $1.to_i
+ if ind > 0 and ind <= self.messages.length
+ d << "+OK #{ind} #{self.messages[ind-1].length}"
+ else
+ d << "-ERR Unknown message."
+ end
+ else
+ d << "+OK #{self.messages.length} messages (#{self.messages.inject(0){|s,m| s+= m[1].length}} octets)."
+ self.messages.each_with_index{|m,i| d << "#{i+1} #{m.length}"}
+ d << "."
+ end
+
+ send_data d.join(CRLF)
+ end
+
+ def do_process_uidl(a)
+ if a =~ /\AUIDL +(\d+)\b/
+ ind = $1.to_i
+ if ind > 0 and ind <= self.messages.length
+ m = self.messages[ind-1][0].id
+ send_data "+OK #{ind} #{m}"
+ else
+ send_data "-ERR Message not found."
+ end
+ else
+ d = ["+OK "]
+ self.messages.each_with_index{|m,i| d << "#{i+1} #{m[0].id}"}
+ d << "."
+
+ send_data d.join(CRLF)
+ end
+ end
+
+ def do_process_retr(a)
+ if a =~ /\ARETR +(\d+)\b/
+ ind = $1.to_i
+ if ind > 0 and ind <= self.messages.length
+ alert_changed, msg = self.messages[ind-1]
+ send_data ["+OK #{msg.length} octets", msg, "."].join(CRLF)
+ note = "#{alert_changed.update_type.capitalize} notification downloaded via POP3 by #{@user}"
+ logger.info note+" about #{alert_changed}."
+ h = History.new(:alert_id => alert_changed.alert_id, :type => "notification", :event => note)
+ logger.error "Unable to save history due to #{h.errors.inspect}" if !h.save
+ else
+ send_data "-ERR Message not found."
+ end
+ else
+ send_data "-ERR Boo."
+ end
+
+ end
+
+ #
+ # These are the messages in the mailbox.
+ #
+ def messages
+ if @messages.empty?
+ @messages = []
+ smtp = Mauve::Notifiers::Email::Default.new("TODO: why do I need to put this argument here?")
+ alerts_seen = []
+
+ AlertChanged.all(:person => self.user).each do |a|
+ #
+ # Not interested in alerts
+ #
+ next unless @level.nil? or a.level.to_s == @level
+
+ #
+ # Only one message per alert.
+ #
+ next if alerts_seen.include?([a.alert_id, a.update_type])
+
+ relevant = case a.update_type
+ when "raised"
+ a.alert.raised?
+ when "acknowledged"
+ a.alert.acknowledged?
+ when "cleared"
+ a.alert.cleared?
+ else
+ false
+ end
+
+ next unless relevant
+
+ alerts_seen << [a.alert_id, a.update_type]
+
+ @messages << [a, smtp.prepare_message(self.user, a.alert, [])]
+ end
+ end
+
+ @messages
+ end
+
+ end
+
+end
+
diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb
index 307002f..34ea155 100644
--- a/lib/mauve/server.rb
+++ b/lib/mauve/server.rb
@@ -9,6 +9,7 @@ require 'mauve/mauve_thread'
require 'mauve/mauve_time'
require 'mauve/timer'
require 'mauve/udp_server'
+require 'mauve/pop3_server'
require 'mauve/processor'
require 'mauve/http_server'
require 'mauve/heartbeat'
@@ -21,7 +22,7 @@ module Mauve
#
# This is the order in which the threads should be started.
#
- THREAD_CLASSES = [UDPServer, HTTPServer, Processor, Timer, Notifier, Heartbeat]
+ THREAD_CLASSES = [UDPServer, HTTPServer, Pop3Server, Processor, Timer, Notifier, Heartbeat]
attr_reader :hostname, :database, :initial_sleep
attr_reader :packet_buffer, :notification_buffer, :started_at
diff --git a/lib/mauve/version.rb b/lib/mauve/version.rb
index 583741d..9967f49 100644
--- a/lib/mauve/version.rb
+++ b/lib/mauve/version.rb
@@ -1,5 +1,5 @@
module Mauve
- VERSION="3.2.1"
+ VERSION="3.3.0"
end
diff --git a/lib/mauve/web_interface.rb b/lib/mauve/web_interface.rb
index 9369ee3..805a3e3 100644
--- a/lib/mauve/web_interface.rb
+++ b/lib/mauve/web_interface.rb
@@ -3,6 +3,8 @@ require 'haml'
require 'redcloth'
require 'json'
+require 'mauve/authentication'
+
require 'sinatra/tilt'
require 'sinatra/base'
require 'sinatra-partials'
@@ -148,7 +150,7 @@ EOF
#
next_page = '/' if next_page == '/logout'
- if auth_helper(usr, pwd)
+ if Authentication.authenticate(usr, pwd)
session['username'] = usr
redirect next_page
else
@@ -463,52 +465,6 @@ EOF
list[@cycle]
end
- ## Test for authentication with SSO.
- #
- def auth_helper (usr, pwd)
- # First try Bytemark
- #
- auth = AuthBytemark.new()
- result = begin
- auth.authenticate(usr,pwd)
- rescue Exception => ex
- logger.error "Caught exception during Bytemark auth for #{usr} (#{ex.to_s})"
- logger.debug ex.backtrace.join("\n")
- false
- end
-
- if true == result
- return true
- else
- logger.warn "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.error "Caught exception during local auth for #{usr} (#{ex.to_s})"
- logger.debug ex.backtrace.join("\n")
- false
- end
-
- if true == result
- return true
- else
- logger.warn "Local authentication failed for #{usr}"
- end
-
- #
- # Rate limit logins.
- #
- sleep 5
- false
- end
-
end
error DataMapper::ObjectNotFoundError do