# encoding: utf-8
# Copyright 2014-2020 Aerospike, Inc.
#
# Portions may be licensed to Aerospike, Inc. under one or more contributor
# license agreements.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

require 'aerospike/command/admin_command'

module Aerospike

  private

  attr_reader :session_token, :session_expiration

  class LoginCommand < AdminCommand #:nodoc:

    def login(conn, policy)
        hashed_pass = LoginCommand.hash_password(policy.password)
        authenticate(conn, policy, hashed_pass)
    end

    def authenticate(conn, user, hashed_pass)
      write_header(LOGIN, 2)
      write_field_str(USER, policy.user)
      write_field_bytes(CREDENTIAL, hashed_pass)

      parse_tokens(conn)
    end

    def authenticate_new(conn, cluster)
      @data_offset = 8
      policy = cluster.client_policy
      case policy.auth_mode
      when Aerospike::AuthMode::EXTERNAL
        write_header(LOGIN, 3)
        write_field_str(USER, policy.user)
        write_field_bytes(CREDENTIAL, cluster.password)
        write_field_str(CLEAR_PASSWORD, policy.password)
      when Aerospike::AuthMode::INTERNAL
        write_header(LOGIN, 2)
        write_field_str(USER, policy.user)
        write_field_bytes(CREDENTIAL, cluster.password)
      when Aerospike::AuthMode::PKI
        write_header(LOGIN, 0)
      else
        raise Exceptions::Aerospike.new(Aerospike::ResultCode::COMMAND_REJECTED, "Invalid client_policy#auth_mode.")
      end

      parse_tokens(conn)
      cluster.session_token = @session_token
      cluster.session_expiration = @session_expiration
    end

    def parse_tokens(conn)
      begin
        write_size
        conn.write(@data_buffer, @data_offset)
        conn.read(@data_buffer, HEADER_SIZE)

        result = @data_buffer.read(RESULT_CODE)

        if result != 0
          return if result == Aerospike::ResultCode::SECURITY_NOT_ENABLED
          raise Exceptions::Aerospike.new(result, "Authentication failed")
        end

        # read the rest of the buffer
        size = @data_buffer.read_int64(0)
        receive_size = (size & 0xFFFFFFFFFFFF) - HEADER_REMAINING
        field_count = @data_buffer.read(11) & 0xFF

        if receive_size <= 0 || receive_size > @data_buffer.size || field_count <= 0
          raise Exceptions::Aerospike.new(result, "Node failed to retrieve session token")
        end

        if @data_buffer.size < receive_size
          @data_buffer.resize(receive_size)
        end

        conn.read(@data_buffer, receive_size)

        @data_offset = 0
        for i in 0...field_count
          mlen = @data_buffer.read_int32(@data_offset)
          @data_offset += 4
          id = @data_buffer.read(@data_offset)
          @data_offset += 1
          mlen -= 1

          case id
          when SESSION_TOKEN
            # copy the contents of the buffer into a new byte slice
            @session_token = @data_buffer.read(@data_offset, mlen)

          when SESSION_TTL
            # Subtract 60 seconds from TTL so client session expires before server session.
            seconds = @data_buffer.read_int32(@data_offset) - 60

            if seconds > 0
              @session_expiration = Time.now + (seconds/86400)
            else
              Aerospike.logger.warn("Invalid session TTL: #{seconds}")
              raise Exceptions::Aerospike.new(result, "Node failed to retrieve session token")
            end
          end

          @data_offset += mlen
        end

        if !@session_token
          raise Exceptions::Aerospike.new(result, "Node failed to retrieve session token")
        end
      ensure
        Buffer.put(@data_buffer)
      end
    end

    def authenticate_via_token(conn, cluster)
      @data_offset = 8
      policy = cluster.client_policy
      if policy.auth_mode == Aerospike::AuthMode::PKI
        write_header(AUTHENTICATE, 1)
      else
        write_header(AUTHENTICATE, 2)
        write_field_str(USER, policy.user)
      end

      write_field_bytes(SESSION_TOKEN, cluster.session_token) if cluster.session_token
      write_size

      conn.write(@data_buffer, @data_offset)
      conn.read(@data_buffer, HEADER_SIZE)

      result = @data_buffer.read(RESULT_CODE)
      size = @data_buffer.read_int64(0)
      receive_size = (size & 0xFFFFFFFFFFFF) - HEADER_REMAINING
      conn.read(@data_buffer, receive_size)

      if result != 0
        return if result == Aerospike::ResultCode::SECURITY_NOT_ENABLED
        raise Exceptions::Aerospike.new(result, "Authentication failed")
      end

      nil
    end

    SALT = '$2a$10$7EqJtq98hPqEX7fNZaFWoO'
    def self.hash_password(password)
      # Hashing the password with the cost of 10, with a static salt
      return BCrypt::Engine.hash_secret(password, SALT, :cost => 10)
    end
  end
end

