local constants = require "kong.constants"
local openssl_mac = require "resty.openssl.mac"


local sha256_base64 = require("kong.tools.sha256").sha256_base64
local splitn = require("kong.tools.string").splitn


local ngx = ngx
local kong = kong
local error = error
local time = ngx.time
local abs = math.abs
local decode_base64 = ngx.decode_base64
local parse_time = ngx.parse_http_time
local re_gmatch = ngx.re.gmatch
local hmac_sha1 = ngx.hmac_sha1
local ipairs = ipairs
local fmt = string.format
local string_lower = string.lower
local kong_request = kong.request
local kong_client = kong.client
local kong_service_request = kong.service.request


local AUTHORIZATION = "authorization"
local PROXY_AUTHORIZATION = "proxy-authorization"
local DATE = "date"
local X_DATE = "x-date"
local DIGEST = "digest"
local SIGNATURE_NOT_VALID = "HMAC signature cannot be verified"
local SIGNATURE_NOT_SAME = "HMAC signature does not match"


local hmac = {
  ["hmac-sha1"] = function(secret, data)
    return hmac_sha1(secret, data)
  end,
  ["hmac-sha256"] = function(secret, data)
    return openssl_mac.new(secret, "HMAC", nil, "sha256"):final(data)
  end,
  ["hmac-sha384"] = function(secret, data)
    return openssl_mac.new(secret, "HMAC", nil, "sha384"):final(data)
  end,
  ["hmac-sha512"] = function(secret, data)
    return openssl_mac.new(secret, "HMAC", nil, "sha512"):final(data)
  end,
}


local function list_as_set(list)
  local set = kong.table.new(0, #list)
  for _, v in ipairs(list) do
    set[v] = true
  end

  return set
end


local function validate_params(params, conf)
  -- check username and signature are present
  if not params.username or not params.signature then
    return nil, "username or signature missing"
  end

  -- check enforced headers are present
  if conf.enforce_headers and #conf.enforce_headers >= 1 then
    local enforced_header_set = list_as_set(conf.enforce_headers)

    if params.hmac_headers then
      for _, header in ipairs(params.hmac_headers) do
        enforced_header_set[header] = nil
      end
    end

    for _, header in ipairs(conf.enforce_headers) do
      if enforced_header_set[header] then
        return nil, "enforced header not used for signature creation"
      end
    end
  end

  -- check supported alorithm used
  for _, algo in ipairs(conf.algorithms) do
    if algo == params.algorithm then
      return true
    end
  end

  return nil, fmt("algorithm %s not supported", params.algorithm)
end


local function retrieve_hmac_fields(authorization_header)
  local hmac_params = {}

  -- parse the header to retrieve hamc parameters
  if authorization_header then
    local iterator, iter_err = re_gmatch(authorization_header,
                                         "\\s*[Hh]mac\\s*username=\"(.+)\"," ..
                                         "\\s*algorithm=\"(.+)\",\\s*header" ..
                                         "s=\"(.+)\",\\s*signature=\"(.+)\"",
                                         "jo")
    if not iterator then
      kong.log.err(iter_err)
      return
    end

    local m, err = iterator()
    if err then
      kong.log.err(err)
      return
    end

    if m and #m >= 4 then
      hmac_params.username = m[1]
      hmac_params.algorithm = m[2]
      hmac_params.hmac_headers = splitn(m[3], " ")
      hmac_params.signature = m[4]
    end
  end

  return hmac_params
end


-- plugin assumes the request parameters being used for creating
-- signature by client are not changed by core or any other plugin
local function create_hash(request_uri, hmac_params)
  local signing_string = ""
  local hmac_headers = hmac_params.hmac_headers

  local count = #hmac_headers
  for i = 1, count do
    local header = hmac_headers[i]
    local header_value = kong.request.get_header(header)

    if not header_value then
      if header == "@request-target" then
        local request_target = string_lower(kong.request.get_method()) .. " " .. request_uri
        signing_string = signing_string .. header .. ": " .. request_target

      elseif header == "request-line" then
        -- request-line in hmac headers list
        local request_line = fmt("%s %s HTTP/%.01f",
                                 kong_request.get_method(),
                                 request_uri,
                                 assert(kong_request.get_http_version()))
        signing_string = signing_string .. request_line

      else
        signing_string = signing_string .. header .. ":"
      end

    else
      signing_string = signing_string .. header .. ":" .. " " .. header_value
    end

    if i < count then
      signing_string = signing_string .. "\n"
    end
  end

  return hmac[hmac_params.algorithm](hmac_params.secret, signing_string)
end


local function validate_signature(hmac_params)
  local signature_1 = create_hash(kong_request.get_path_with_query(), hmac_params)
  local signature_2 = decode_base64(hmac_params.signature)
  return signature_1 == signature_2
end


local function load_credential_into_memory(username)
  local key, err = kong.db.hmacauth_credentials:select_by_username(username)
  if err then
    return nil, err
  end
  return key
end


local function load_credential(username)
  local credential, err
  if username then
    local credential_cache_key = kong.db.hmacauth_credentials:cache_key(username)
    credential, err = kong.cache:get(credential_cache_key, nil,
                                     load_credential_into_memory,
                                     username)
  end

  if err then
    return error(err)
  end

  return credential
end


local function validate_clock_skew(date_header_name, allowed_clock_skew)
  local date = kong_request.get_header(date_header_name)
  if not date then
    return false
  end

  local requestTime = parse_time(date)
  if not requestTime then
    return false
  end

  local skew = abs(time() - requestTime)
  if skew > allowed_clock_skew then
    return false
  end

  return true
end


local function validate_body()
  local body, err = kong_request.get_raw_body()
  if err then
    kong.log.debug(err)
    return false
  end

  local digest_received = kong_request.get_header(DIGEST)
  if not digest_received then
    -- if there is no digest and no body, it is ok
    return body == ""
  end

  local digest_created = "SHA-256=" .. sha256_base64(body or '')

  return digest_created == digest_received
end


local function set_consumer(consumer, credential)
  kong_client.authenticate(consumer, credential)

  local set_header = kong_service_request.set_header
  local clear_header = kong_service_request.clear_header

  if consumer and consumer.id then
    set_header(constants.HEADERS.CONSUMER_ID, consumer.id)
  else
    clear_header(constants.HEADERS.CONSUMER_ID)
  end

  if consumer and consumer.custom_id then
    set_header(constants.HEADERS.CONSUMER_CUSTOM_ID, consumer.custom_id)
  else
    clear_header(constants.HEADERS.CONSUMER_CUSTOM_ID)
  end

  if consumer and consumer.username then
    set_header(constants.HEADERS.CONSUMER_USERNAME, consumer.username)
  else
    clear_header(constants.HEADERS.CONSUMER_USERNAME)
  end

  if credential and credential.username then
    set_header(constants.HEADERS.CREDENTIAL_IDENTIFIER, credential.username)
  else
    clear_header(constants.HEADERS.CREDENTIAL_IDENTIFIER)
  end

  if credential then
    clear_header(constants.HEADERS.ANONYMOUS)
  else
    set_header(constants.HEADERS.ANONYMOUS, true)
  end
end

local function unauthorized(message, www_auth_content)
  return { status = 401, message = message, headers = { ["WWW-Authenticate"] = www_auth_content } }
end


local function do_authentication(conf)
  local authorization = kong_request.get_header(AUTHORIZATION)
  local proxy_authorization = kong_request.get_header(PROXY_AUTHORIZATION)
  local www_auth_content = conf.realm and fmt('hmac realm="%s"', conf.realm) or 'hmac'

  -- If both headers are missing, return 401
  if not (authorization or proxy_authorization) then
    return false, unauthorized("Unauthorized", www_auth_content)
  end

  -- validate clock skew
  if not (validate_clock_skew(X_DATE, conf.clock_skew) or
          validate_clock_skew(DATE, conf.clock_skew)) then
    return false, unauthorized(
      "HMAC signature cannot be verified, a valid date or " ..
      "x-date header is required for HMAC Authentication",
      www_auth_content
    )
  end

  -- retrieve hmac parameter from Proxy-Authorization header
  local hmac_params = retrieve_hmac_fields(proxy_authorization)

  -- Try with the authorization header
  if not hmac_params.username then
    hmac_params = retrieve_hmac_fields(authorization)
    if hmac_params and conf.hide_credentials then
      kong_service_request.clear_header(AUTHORIZATION)
    end

  elseif conf.hide_credentials then
    kong_service_request.clear_header(PROXY_AUTHORIZATION)
  end

  local ok, err = validate_params(hmac_params, conf)
  if not ok then
    kong.log.debug(err)
    return false, unauthorized(SIGNATURE_NOT_VALID, www_auth_content)
  end

  -- validate signature
  local credential = load_credential(hmac_params.username)
  if not credential then
    kong.log.debug("failed to retrieve credential for ", hmac_params.username)
    return false, unauthorized(SIGNATURE_NOT_VALID, www_auth_content)
  end

  hmac_params.secret = credential.secret

  if not validate_signature(hmac_params) then
    return false, unauthorized(SIGNATURE_NOT_SAME, www_auth_content)
  end

  -- If request body validation is enabled, then verify digest.
  if conf.validate_request_body and not validate_body() then
    kong.log.debug("digest validation failed")
    return false, unauthorized(SIGNATURE_NOT_SAME, www_auth_content)
  end

  -- Retrieve consumer
  local consumer_cache_key, consumer
  consumer_cache_key = kong.db.consumers:cache_key(credential.consumer.id)
  consumer, err      = kong.cache:get(consumer_cache_key, nil,
                                      kong_client.load_consumer,
                                      credential.consumer.id)
  if err then
    return error(err)
  end

  set_consumer(consumer, credential)

  return true
end

local function set_anonymous_consumer(anonymous)
  local consumer_cache_key = kong.db.consumers:cache_key(anonymous)
  local consumer, err = kong.cache:get(consumer_cache_key, nil,
                                        kong.client.load_consumer,
                                        anonymous, true)
  if err then
    return error(err)
  end

  if not consumer then
    local err_msg = "anonymous consumer " .. anonymous .. " is configured but doesn't exist"
    kong.log.err(err_msg)
    return kong.response.error(500, err_msg)
  end

  set_consumer(consumer)
end

--- When conf.anonymous is enabled we are in "logical OR" authentication flow.
--- Meaning - either anonymous consumer is enabled or there are multiple auth plugins
--- and we need to passthrough on failed authentication.
local function logical_OR_authentication(conf)
  if kong.client.get_credential() then
    -- we're already authenticated and in "logical OR" between auth methods -- early exit
    return
  end

  local ok, _ = do_authentication(conf)
  if not ok then
    set_anonymous_consumer(conf.anonymous)
  end
end

--- When conf.anonymous is not set we are in "logical AND" authentication flow.
--- Meaning - if this authentication fails the request should not be authorized
--- even though other auth plugins might have successfully authorized user.
local function logical_AND_authentication(conf)
  local ok, err = do_authentication(conf)
  if not ok then
    return kong.response.error(err.status, err.message, err.headers)
  end
end


local _M = {}

function _M.execute(conf)

  if conf.anonymous then
    return logical_OR_authentication(conf)
  else
    return logical_AND_authentication(conf)
  end
end


return _M
