--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements.  See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You 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.
--
local core = require("apisix.core")
local ngx = ngx
local ngx_re = require("ngx.re")
local consumer = require("apisix.consumer")
local schema_def = require("apisix.schema_def")
local auth_utils = require("apisix.utils.auth")

local lrucache = core.lrucache.new({
    ttl = 300, count = 512
})

local schema = {
    type = "object",
    title = "work with route or service object",
    properties = {
        hide_credentials = {
            type = "boolean",
            default = false,
        },
        realm = schema_def.get_realm_schema("basic"),
    },
    anonymous_consumer = schema_def.anonymous_consumer_schema,
}

local consumer_schema = {
    type = "object",
    title = "work with consumer object",
    properties = {
        username = { type = "string" },
        password = { type = "string" },
    },
    encrypt_fields = {"password"},
    required = {"username", "password"},
}

local plugin_name = "basic-auth"


local _M = {
    version = 0.1,
    priority = 2520,
    type = 'auth',
    name = plugin_name,
    schema = schema,
    consumer_schema = consumer_schema
}

function _M.check_schema(conf, schema_type)
    local ok, err
    if schema_type == core.schema.TYPE_CONSUMER then
        ok, err = core.schema.check(consumer_schema, conf)
    else
        ok, err = core.schema.check(schema, conf)
    end

    if not ok then
        return false, err
    end

    return true
end

local function extract_auth_header(authorization)

    local function do_extract(auth)
        local obj = { username = "", password = "" }

        local m, err = ngx.re.match(auth, "(?i:basic)\\s(.+)", "jo")
        if err then
            -- error authorization
            return nil, err
        end

        if not m then
            return nil, "Invalid authorization header format"
        end

        local decoded = ngx.decode_base64(m[1])

        if not decoded then
            return nil, "Failed to decode authentication header: " .. m[1]
        end

        local res
        res, err = ngx_re.split(decoded, ":")
        if err then
            return nil, "Split authorization err:" .. err
        end
        if #res < 2 then
            return nil, "Split authorization err: invalid decoded data: " .. decoded
        end

        obj.username = ngx.re.gsub(res[1], "\\s+", "", "jo")
        obj.password = ngx.re.gsub(res[2], "\\s+", "", "jo")
        return obj, nil
    end

    local matcher, err = lrucache(authorization, nil, do_extract, authorization)

    if matcher then
        return matcher.username, matcher.password, err
    else
        return "", "", err
    end

end


local function find_consumer(ctx)
    local auth_header = core.request.header(ctx, "Authorization")
    if not auth_header then
        return nil, nil, "Missing authorization in request"
    end

    local username, password, err = extract_auth_header(auth_header)
    if err then
        if auth_utils.is_running_under_multi_auth(ctx) then
            return nil, nil, err
        end
        core.log.warn(err)
        return nil, nil, "Invalid authorization in request"
    end

    local cur_consumer, consumer_conf, err = consumer.find_consumer(plugin_name,
                                             "username", username)
    if not cur_consumer then
        err = "failed to find user: " .. (err or "invalid user")
        if auth_utils.is_running_under_multi_auth(ctx) then
            return nil, nil, err
        end
        core.log.warn(err)
        return nil, nil, "Invalid user authorization"
    end

    if cur_consumer.auth_conf.password ~= password then
        return nil, nil, "Invalid user authorization"
    end

    return cur_consumer, consumer_conf, err
end


function _M.rewrite(conf, ctx)
    local cur_consumer, consumer_conf, err = find_consumer(ctx, conf)
    if not cur_consumer then
        if not conf.anonymous_consumer then
            core.response.set_header("WWW-Authenticate", "Basic realm=\"" .. conf.realm .. "\"")
            return 401, { message = err }
        end
        cur_consumer, consumer_conf, err = consumer.get_anonymous_consumer(conf.anonymous_consumer)
        if not cur_consumer then
            err = "basic-auth failed to authenticate the request, code: 401. error: " .. err
            core.log.error(err)
            core.response.set_header("WWW-Authenticate", "Basic realm=\"" .. conf.realm .. "\"")
            return 401, { message = "Invalid user authorization" }
        end
    end
    if conf.hide_credentials then
        core.request.set_header(ctx, "Authorization", nil)
    end

    consumer.attach_consumer(ctx, cur_consumer, consumer_conf)

    core.log.info("hit basic-auth access")
end

return _M
