# This file is part of the ruby-dbus project
# Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License, version 2.1 as published by the Free Software Foundation.
# See the file "COPYING" for the exact licensing terms.

require 'rbconfig'

module DBus
  # Exception raised when authentication fails somehow.
  class AuthenticationFailed < Exception
  end

  # = General class for authentication.
  class Authenticator
    # Returns the name of the authenticator.
    def name
      self.class.to_s.upcase.sub(/.*::/, "")
    end
  end

  # = Anonymous authentication class
  class Anonymous < Authenticator
    def authenticate
      '527562792044427573' # Hex encoded version of "Ruby DBus"
    end
  end

  # = External authentication class 
  #
  # Class for 'external' type authentication.
  class External < Authenticator
    # Performs the authentication.
    def authenticate
      # Take the user id (eg integer 1000) make a string out of it "1000", take
      # each character and determin hex value "1" => 0x31, "0" => 0x30. You
      # obtain for "1000" => 31303030 This is what the server is expecting.
      # Why? I dunno. How did I come to that conclusion? by looking at rbus
      # code. I have no idea how he found that out.
      return Process.uid.to_s.split(//).collect { |a| "%x" % a[0].ord }.join
    end
  end
  
  # = Authentication class using SHA1 crypto algorithm 
  #
  # Class for 'CookieSHA1' type authentication.
  # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
  class DBusCookieSHA1 < Authenticator
        
    #the autenticate method (called in stage one of authentification)    
    def authenticate
      require 'etc'
      #number of retries we have for auth
      @retries = 1
      return "#{hex_encode(Etc.getlogin)}" #server expects it to be binary
    end

    #returns the modules name
    def name
      return 'DBUS_COOKIE_SHA1'
    end
    
    #handles the interesting crypto stuff, check the rbus-project for more info: http://rbus.rubyforge.org/
    def data(hexdata)
      require 'digest/sha1'
      data = hex_decode(hexdata)
      # name of cookie file, id of cookie in file, servers random challenge  
      context, id, s_challenge = data.split(' ')
      # Random client challenge        
      c_challenge = Array.new(s_challenge.bytesize/2).map{|obj|obj=rand(255).to_s}.join
      # Search cookie file for id
      path = File.join(ENV['HOME'], '.dbus-keyrings', context)
      DBus.logger.debug "path: #{path.inspect}"
      File.foreach(path) do |line|
        if line.index(id) == 0
          # Right line of file, read cookie
          cookie = line.split(' ')[2].chomp
          DBus.logger.debug "cookie: #{cookie.inspect}"
          # Concatenate and encrypt
          to_encrypt = [s_challenge, c_challenge, cookie].join(':')
          sha = Digest::SHA1.hexdigest(to_encrypt)
          #the almighty tcp server wants everything hex encoded
          hex_response = hex_encode("#{c_challenge} #{sha}")
          # Return response
          response = [:AuthOk, hex_response]
          return response
        end
      end
      #a little rescue magic
      unless @retries <= 0
        puts "ERROR: Could not auth, will now exit." 
        puts "ERROR: Unable to locate cookie, retry in 1 second."
        @retries -= 1
        sleep 1
        data(hexdata)
      end
    end  
    
    # encode plain to hex
    def hex_encode(plain)
      return nil if plain.nil?
      plain.to_s.unpack('H*')[0]
    end
    
    # decode hex to plain
    def hex_decode(encoded)
      encoded.scan(/[[:xdigit:]]{2}/).map{|h|h.hex.chr}.join
    end  
  end #DBusCookieSHA1 class ends here

  # Note: this following stuff is tested with External authenticator only!

  # = Authentication client class.
  #
  # Class tha performs the actional authentication.
  class Client
    # Create a new authentication client.
    def initialize(socket)
      @socket = socket
      @state = nil
      @auth_list = [External,DBusCookieSHA1,Anonymous]
    end

    # Start the authentication process.
    def authenticate
      if (RbConfig::CONFIG["target_os"] =~ /freebsd/)
        @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
      else
        @socket.write(0.chr)
      end
      next_authenticator
      @state = :Starting
      while @state != :Authenticated
        r = next_state
        return r if not r
      end
      true
    end

    ##########
    private
    ##########

    # Send an authentication method _meth_ with arguments _args_ to the
    # server.
    def send(meth, *args)
      o = ([meth] + args).join(" ")
      @socket.write(o + "\r\n")
    end

    # Try authentication using the next authenticator.
    def next_authenticator
      begin
        raise AuthenticationFailed if @auth_list.size == 0
        @authenticator = @auth_list.shift.new
        auth_msg = ["AUTH", @authenticator.name, @authenticator.authenticate]
        DBus.logger.debug "auth_msg: #{auth_msg.inspect}"
        send(auth_msg)
      rescue AuthenticationFailed
        @socket.close
        raise
      end
    end

    # Read data (a buffer) from the bus until CR LF is encountered.
    # Return the buffer without the CR LF characters.
    def next_msg
      data,crlf = "","\r\n"
      left = 1024 #1024 byte, no idea if it's ever getting bigger
      while left > 0
        buf = @socket.read( left > 1 ? 1 : left )
        break if buf.nil?
        left -= buf.bytesize
        data += buf
        break if data.include? crlf #crlf means line finished, the TCP socket keeps on listening, so we break 
      end
      readline = data.chomp.split(" ")
      DBus.logger.debug "readline: #{readline.inspect}"
      return readline
    end

=begin
    # Read data (a buffer) from the bus until CR LF is encountered.
    # Return the buffer without the CR LF characters.
    def next_msg
      @socket.readline.chomp.split(" ")
    end
=end

    # Try to reach the next state based on the current state.
    def next_state
      msg = next_msg
      if @state == :Starting
        DBus.logger.debug ":Starting msg: #{msg[0].inspect}"
        case msg[0]
        when "OK"
          @state = :WaitingForOk    
        when "CONTINUE"
          @state = :WaitingForData
        when "REJECTED" #needed by tcp, unix-path/abstract doesn't get here
          @state = :WaitingForData
        end
      end
      DBus.logger.debug "state: #{@state}"
      case @state
      when :WaitingForData
        DBus.logger.debug ":WaitingForData msg: #{msg[0].inspect}"
        case msg[0]
        when "DATA"
          chall = msg[1]
          resp, chall = @authenticator.data(chall)
          DBus.logger.debug ":WaitingForData/DATA resp: #{resp.inspect}"
          case resp
          when :AuthContinue
            send("DATA", chall)
            @state = :WaitingForData
          when :AuthOk
            send("DATA", chall)
            @state = :WaitingForOk
          when :AuthError
            send("ERROR")
            @state = :WaitingForData
          end
        when "REJECTED"
          next_authenticator
          @state = :WaitingForData
        when "ERROR"
          send("CANCEL")
          @state = :WaitingForReject
        when "OK"
          send("BEGIN")
          @state = :Authenticated
        else
          send("ERROR")
          @state = :WaitingForData
        end
      when :WaitingForOk
        DBus.logger.debug ":WaitingForOk msg: #{msg[0].inspect}"
        case msg[0]
        when "OK"
          send("BEGIN")
          @state = :Authenticated
        when "REJECT"
          next_authenticator
          @state = :WaitingForData
        when "DATA", "ERROR"
          send("CANCEL")
          @state = :WaitingForReject
        else
          send("ERROR")
          @state = :WaitingForOk
        end
      when :WaitingForReject
        DBus.logger.debug ":WaitingForReject msg: #{msg[0].inspect}"
        case msg[0]
        when "REJECT"
          next_authenticator
          @state = :WaitingForOk
        else
          @socket.close
          return false
        end
      end
      return true
    end # def next_state
  end # class Client
end # module D-Bus
