diff options
author | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-08-04 14:27:01 +0100 |
---|---|---|
committer | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-08-04 14:27:01 +0100 |
commit | 451e7fac0b7cb6c7cb3659c8a4acceaab6125123 (patch) | |
tree | ff43cd35e8e828f7b2a39e00ecaceb6e44cd49d1 | |
parent | f07c5705d4ec82f4ba851409d4a92a9f66820df8 (diff) |
Added pop3 server.
--HG--
rename : lib/mauve/auth_bytemark.rb => lib/mauve/authentication.rb
-rw-r--r-- | debian/changelog | 8 | ||||
-rw-r--r-- | lib/mauve/auth_bytemark.rb | 55 | ||||
-rw-r--r-- | lib/mauve/authentication.rb | 125 | ||||
-rw-r--r-- | lib/mauve/http_server.rb | 21 | ||||
-rw-r--r-- | lib/mauve/notifiers/email.rb | 4 | ||||
-rw-r--r-- | lib/mauve/pop3_server.rb | 315 | ||||
-rw-r--r-- | lib/mauve/server.rb | 3 | ||||
-rw-r--r-- | lib/mauve/version.rb | 2 | ||||
-rw-r--r-- | lib/mauve/web_interface.rb | 50 |
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 |