diff options
Diffstat (limited to 'lib/mauve')
| -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 | 
8 files changed, 461 insertions, 114 deletions
| 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 | 
