class DBus::Authentication::Client

Authenticates the connection before messages can be exchanged.

Attributes

address_uuid[R]

@return [String]

unix_fd[R]

@return [Boolean] have we negotiated Unix file descriptor passing NOTE: not implemented yet in upper layers

Public Class Methods

new(socket, mechs = nil) click to toggle source

Create a new authentication client. @param mechs [Array<Mechanism,Class>,nil] custom list of auth Mechanism objects or classes

# File lib/dbus/auth.rb, line 133
def initialize(socket, mechs = nil)
  @unix_fd = false
  @address_uuid = nil

  @socket = socket
  @state = nil
  @auth_list = mechs || [
    External,
    DBusCookieSHA1,
    ExternalWithoutUid,
    Anonymous
  ]
end

Public Instance Methods

authenticate() click to toggle source

Start the authentication process. @return [void] @raise [AuthenticationFailed]

# File lib/dbus/auth.rb, line 150
def authenticate
  DBus.logger.debug "Authenticating"
  send_nul_byte

  use_next_mechanism

  @state, command = next_state_via_mechanism.to_a
  send(command)

  loop do
    DBus.logger.debug "auth STATE: #{@state}"
    words = next_msg

    @state, command = next_state(words).to_a
    break if [:TerminatedOk, :TerminatedError].include? @state

    send(command)
  end

  raise AuthenticationFailed, command.first if @state == :TerminatedError

  send("BEGIN")
end

Private Instance Methods

hex_decode(encoded) click to toggle source

decode hex to plain @param encoded [String,nil] @return [String,nil]

# File lib/dbus/auth.rb, line 203
def hex_decode(encoded)
  return nil if encoded.nil?

  [encoded].pack("H*")
end
hex_encode(plain) click to toggle source

encode plain to hex @param plain [String,nil] @return [String,nil]

# File lib/dbus/auth.rb, line 194
def hex_encode(plain)
  return nil if plain.nil?

  plain.unpack1("H*")
end
next_msg() click to toggle source

Read data (a buffer) from the bus until CR LF is encountered. Return the buffer without the CR LF characters. @return [Array<String>] received words

# File lib/dbus/auth.rb, line 239
def next_msg
  read_line.chomp.split(" ")
end
next_state(received_words) click to toggle source

Try to reach the next state based on the current state. @param received_words [Array<String>] @return [NextState]

# File lib/dbus/auth.rb, line 295
def next_state(received_words)
  msg = received_words

  case @state
  when :WaitingForData
    case msg[0]
    when "DATA"
      next_state_via_mechanism(msg[1], use_data: true)
    when "REJECTED"
      use_next_mechanism
      next_state_via_mechanism
    when "ERROR"
      NextState.new(:WaitingForReject, ["CANCEL"])
    when "OK"
      @address_uuid = msg[1]
      # NextState.new(:TerminatedOk, [])
      NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
    else
      NextState.new(:WaitingForData, ["ERROR"])
    end
  when :WaitingForOk
    case msg[0]
    when "OK"
      @address_uuid = msg[1]
      # NextState.new(:TerminatedOk, [])
      NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
    when "REJECTED"
      use_next_mechanism
      next_state_via_mechanism
    when "DATA", "ERROR"
      NextState.new(:WaitingForReject, ["CANCEL"])
    else
      # we don't understand server's response but still wait for a successful auth completion
      NextState.new(:WaitingForOk, ["ERROR"])
    end
  when :WaitingForReject
    case msg[0]
    when "REJECTED"
      use_next_mechanism
      next_state_via_mechanism
    else
      # TODO: spec says to close socket, clarify
      NextState.new(:TerminatedError, ["Unknown server reply #{msg[0].inspect} when expecting REJECTED"])
    end
  when :WaitingForAgreeUnixFD
    case msg[0]
    when "AGREE_UNIX_FD"
      @unix_fd = true
      NextState.new(:TerminatedOk, [])
    when "ERROR"
      @unix_fd = false
      NextState.new(:TerminatedOk, [])
    else
      # TODO: spec says to close socket, clarify
      NextState.new(:TerminatedError, ["Unknown server reply #{msg[0].inspect} to NEGOTIATE_UNIX_FD"])
    end
  else
    raise "Internal error: unhandled state #{@state.inspect}"
  end
end
next_state_via_mechanism(hex_challenge = nil, use_data: false) click to toggle source

@param hex_challenge [String,nil] (nil when the server said “DATArn”) @param use_data [Boolean] say DATA instead of AUTH @return [NextState]

# File lib/dbus/auth.rb, line 271
def next_state_via_mechanism(hex_challenge = nil, use_data: false)
  challenge = hex_decode(hex_challenge)

  action, response = @mechanism.call(challenge)
  DBus.logger.debug "auth mechanism action: #{action.inspect}"

  command = use_data ? ["DATA"] : ["AUTH", @mechanism.name]

  case action
  when :MechError
    NextState.new(:WaitingForData, ["ERROR", response])
  when :MechContinue
    NextState.new(:WaitingForData, command + [hex_encode(response)])
  when :MechOk
    NextState.new(:WaitingForOk, command + [hex_encode(response)])
  else
    raise AuthenticationFailed, "internal error, unknown action #{action.inspect} " \
                                "from our mechanism #{@mechanism.inspect}"
  end
end
read_line() click to toggle source

Read a line from the socket; good place for test mocks. @return [String] CRLF (rn) terminated

# File lib/dbus/auth.rb, line 245
def read_line
  # TODO: probably can simply call @socket.readline
  data = ""
  crlf = "\r\n"
  left = 1024 # 1024 byte, no idea if it's ever getting bigger
  while left.positive?
    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
  DBus.logger.debug "auth_read: #{data.inspect}"
  data
end
send(words) click to toggle source

Send words to the server as a single CRLF terminated string. @param words [Array<String>,String]

# File lib/dbus/auth.rb, line 217
def send(words)
  joined = Array(words).compact.join(" ")
  write_line("#{joined}\r\n")
end
send_nul_byte() click to toggle source

The authentication protocol requires a nul byte that may carry credentials. @return [void]

# File lib/dbus/auth.rb, line 183
def send_nul_byte
  if Platform.freebsd?
    @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
  else
    @socket.write(0.chr)
  end
end
use_next_mechanism() click to toggle source

Try authentication using the next mechanism. @raise [AuthenticationFailed] if there are no more left @return [void]

# File lib/dbus/auth.rb, line 225
def use_next_mechanism
  raise AuthenticationFailed, "Authentication mechanisms exhausted" if @auth_list.empty?

  @mechanism = @auth_list.shift
  @mechanism = @mechanism.new if @mechanism.is_a? Class
rescue AuthenticationFailed
  # TODO: make this caller's responsibility
  @socket.close
  raise
end
write_line(str) click to toggle source

Send a string to the socket; good place for test mocks.

# File lib/dbus/auth.rb, line 210
def write_line(str)
  DBus.logger.debug "auth_write: #{str.inspect}"
  @socket.write(str)
end