Skip to content

Demo config from my production server using Nauthilus as HTTP API server

Christian Rößner edited this page Jan 14, 2025 · 1 revision

Here is a full working example from my production system that demonstrates the use cases:

---
# pfxhttp config file

server:

  listen:
    - kind: "socket_map"
      name: "maps"
      type: "unix"
      address: "/run/pfxhttp/map.sock"
      mode: "0666"

    - kind: "policy_service"
      name: "policies"
      type: "unix"
      address: "/run/pfxhttp/policy.sock"
      mode: "0666"

  logging:
    level: debug

  http_client:
    max_connections_per_host: 10
    max_idle_connections: 4
    max_idle_connections_per_host: 1
    idle_connection_timeout: 10

  tls:
    enabled: true

socket_maps:

  relay_domains:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: relay_domains"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  relay_recipient_maps:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: relay_recipient_maps"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  virtual_alias_maps:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: virtual_alias_maps"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  transport:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: transport"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  quota_flag:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: quota_flag"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  enforce_tls_encryption_in:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: enforce_tls_encryption_in"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  enforce_tls_encryption_out:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: enforce_tls_encryption_out"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  account_status:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: account_status"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  prepend_id:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: prepend_id"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

  login_maps:
    target: https://login.authserv.me/api/v1/custom/pfx
    custom_headers:
      - "Authorization: Basic TOKEN"
      - "X-Pfx-Name: login_maps"
    payload: >
      {
        "key": "{{ .Key }}"
      }
    status_code: 200
    value_field: "result"
    error_field: "error"

policy_services:

  policies:
    target: https://login.authserv.me/api/v1/custom/pfxpolicy
    custom_headers:
      - "Authorization: Basic TOKEN"
    payload: "{{ .Key }}"
    status_code: 200
    value_field: "result"
    error_field: "error"

Here is the Nauthilus pfx endpoint:

local nauthilus_util = require("nauthilus_util")

dynamic_loader("nauthilus_http_request")
local nauthilus_http_request = require("nauthilus_http_request")

dynamic_loader("nauthilus_ldap")
local nauthilus_ldap = require("nauthilus_ldap")

dynamic_loader("nauthilus_gll_json")
local json = require("json")

dynamic_loader("nauthilus_redis")
local nauthilus_redis = require("nauthilus_redis")

dynamic_loader("nauthilus_prometheus")
local nauthilus_prometheus = require("nauthilus_prometheus")

local N = "pfx"

local unpack = table.unpack or unpack
local logs = {}

local REDIS_KEY_EXPIRE = 60

local LDAP_BASE = {
    default_base = "dc=acme,dc=com",
    relay_domains = "ou=mailDomains,dc=acme,dc=com",
}

local LDAP_FILTER = {
    relay_domains = [[
        (&
            (objectClass=rnsMSPostfixGroup)
            (rnsMSVirtDomain=%s)
        )]],
    relay_recipient_maps = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (rnsMSRecipientAddress=%s)
        )]],
    virtual_alias_maps = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (rnsMSRecipientAddress=%s)
        )]],
    transport = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (rnsMSRecipientAddress=%s)
        )]],
    quota_flag = [[
        (&
            (objectClass=rnsMSDovecotAccount)
            (rnsMSRecipientAddress=%s)
        )]],
    enforce_tls_encryption_in = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (rnsMSRequireTls=TRUE)
            (rnsMSRecipientAddress=%s)
        )]],
    enforce_tls_encryption_out = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (rnsMSRequireTls=TRUE)
            (rnsMSRecipientAddress=%s)
        )]],
    account_status = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (rnsMSEnablePostfix=FALSE)
            (|
                (rnsMSRecipientAddress=%s)
                (uniqueIdentifier=%s)
            )
        )]],
    prepend_id = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (rnsMSRecipientAddress=%s)
        )]],
    login_maps = [[
        (&
            (objectClass=rnsMSPostfixAccount)
            (|
                (rnsMSRecipientAddress=%s)
                (uniqueIdentifier=%s)
            )
        )]],
}

local LDAP_ATTRIBUTE = {
    relay_domains = { "rnsMSRejectMsg" },
    relay_recipient_maps = { "rnsMSRecipientAddress" },
    virtual_alias_maps = { "rnsMSDeliverToAddress" },
    transport = { "rnsMSPostfixTransport" },
    quota_flag = { "rnsMSOverQuota" },
    enforce_tls_encryption_in = { "rnsMSRequireTls" },
    enforce_tls_encryption_out = { "rnsMSRequireTls" },
    account_status = { "rnsMSEnablePostfix" },
    prepend_id = { "rnsMSAbuseId" },
    login_maps = { "rnsMSRecipientAddress", "uniqueIdentifier" },
}

local LDAP_SCOPE = {
    BASE = "base",
    SUB = "sub",
}

local function convert_to_string(value)
    if nauthilus_util.is_table(value) then
        return table.concat(value, ",")
    end

    return value
end

local function convert_to_table(value)
    if nauthilus_util.is_string(value) then
        local result = {}
        for word in string.gmatch(value, "([^,]+)") do
            table.insert(result, word)
        end

        if nauthilus_util.table_length(result) > 1 then
            return result
        end
    end

    return value
end

local function get_redis_connection()
    local custom_pool = "default"
    local custom_pool_name =  os.getenv("CUSTOM_REDIS_POOL_NAME")
    if custom_pool_name ~= nil and  custom_pool_name ~= "" then
        local err_redis_client

        custom_pool, err_redis_client = nauthilus_redis.get_redis_connection(custom_pool_name)
        nauthilus_util.if_error_raise(err_redis_client)
    end

    return custom_pool
end

local function cache_load(key, name)
    local redis_key = "ntc:" .. name .. ":" .. key

    local result, err = nauthilus_redis.redis_get(get_redis_connection(), redis_key)
    nauthilus_util.if_error_raise(err)

    return convert_to_table(result)
end

local function cache_save(key, name, value, expire)
    local redis_key = "ntc:" .. name .. ":" .. key

    local _, err = nauthilus_redis.redis_set(get_redis_connection(), redis_key, convert_to_string(value), expire)
    nauthilus_util.if_error_raise(err)
end

local function flatten_ldap_filter(multi_line_string)
    local single_line_string = multi_line_string:gsub("%s+", "")

    return single_line_string
end

local function prepare_filter(format_filter, key)
    local filter = flatten_ldap_filter(format_filter)
    local count = select(2, string.gsub(filter, "%%s", ""))

    local replacements = {}
    for i = 1, count do
        replacements[i] = key
    end

    local result = string.format(filter, unpack(replacements))

    return result
end

local function ldap_search(name, key, session, logging, scope)
    local filter = prepare_filter(LDAP_FILTER[name], key)

    local timer = nauthilus_prometheus.start_histogram_timer(N .. "_duration_seconds", {
        search = name,
    })

    local ldap_result, ldap_err = nauthilus_ldap.ldap_search({
        session = session,
        basedn = LDAP_BASE[name] or LDAP_BASE.default_base,
        filter = filter,
        attributes = LDAP_ATTRIBUTE[name],
        scope = scope,
    })

    nauthilus_prometheus.stop_timer(timer)

    if ldap_result and nauthilus_util.is_table(ldap_result) then
        return ldap_result
    elseif ldap_err then
        nauthilus_util.print_result(logging, logs, "LDAP search result: " .. ldap_err)
    end

    return nil
end

local function process_ldap_result(ldap_result, name)
    local result = {}

    for key, value in pairs(ldap_result) do
        for _, attr_key in ipairs(LDAP_ATTRIBUTE[name]) do
            if key == attr_key then
                if nauthilus_util.is_table(value) then
                    for _, item in ipairs(value) do
                        table.insert(result, item)
                    end

                    break
                else
                    table.insert(result, tostring(value))
                end
            end
        end
    end

    return result
end

-- Results with more than one value
local function handle_list(name, key, session, logging, scope)
    local ldap_result = ldap_search(name, key, session, logging,  scope)
    if ldap_result then
        local result = process_ldap_result(ldap_result, name)
        if nauthilus_util.table_length(result) > 0 then
            return result
        end
    end

    return nil
end

-- Results with exactly one value
local function handle_single_value(name, key, session, logging, scope)
    local ldap_result = ldap_search(name, key, session, logging, scope)
    if ldap_result then
        local result = process_ldap_result(ldap_result, name)
        if nauthilus_util.table_length(result) == 1 then
            return result[1]
        end
    end

    return nil
end

local function handle_name(name, key, session, logging)
    if name == "relay_domains" then
        return handle_single_value(name, key, session, logging, LDAP_SCOPE.BASE)
    elseif name == "relay_recipient_maps" then
        return handle_list(name, key, session, logging, LDAP_SCOPE.SUB)
    elseif name == "virtual_alias_maps" then
        return handle_list(name, key, session, logging, LDAP_SCOPE.SUB)
    elseif name == "transport" then
        return handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
    elseif name == "quota_flag" then
        local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
        if result == nil or string.lower(result) == "false" then
            return "DUNNO"
        else
            return "552 5.2.2 Requested mail action aborted: exceeded storage allocation"
        end
    elseif name == "enforce_tls_encryption_in" then
        local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
        if result and string.lower(result) == "true" then
            return "enforce_tls_encryption"
        end
    elseif name == "enforce_tls_encryption_out" then
        local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
        if result and string.lower(result) == "true" then
            return "smtp-enforced-tls"
        end
    elseif name == "account_status" then
        local result = handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB)
        if result then
            return "REJECT Account suspended"
        end
    elseif name == "prepend_id" then
        return string.format("REPEND X-Abuse-Id: %s", handle_single_value(name, key, session, logging, LDAP_SCOPE.SUB))
    elseif name == "login_maps" then
        return handle_list(name, key, session, logging, LDAP_SCOPE.SUB)
    end

    return nil
end

function nauthilus_run_hook(logging, session)
    logs.level = "info"
    logs.caller = N .. ".lua"
    logs.session = session

    local header = nauthilus_http_request.get_http_request_header("Content-Type")
    local body = nauthilus_http_request.get_http_request_body()

    if nauthilus_util.table_length(header) == 0 or header[1] ~= "application/json" then
        nauthilus_util.print_result(logging, logs, "HTTP request header: Wrong 'Content-Type'")

        return logs
    end

    header = nauthilus_http_request.get_http_request_header("X-Pfx-Name")

    local pfx_name = ""
    if nauthilus_util.table_length(header) == 0 then
        nauthilus_util.print_result(logging, logs, "HTTP request header: Missing required header X-Pfx-Name")

        return logs
    end

    pfx_name = header[1]

    local body_table, err_jdec = json.decode(body)
    nauthilus_util.if_error_raise(err_jdec)

    if not nauthilus_util.is_table(body_table) then
        nauthilus_util.print_result(logging, logs, "HTTP request body: Result is not a table")

        return logs
    end

    local pfx_key = ""
    for k, v in pairs(body_table) do
        if k == "key" then
            if nauthilus_util.is_string(v) then
                pfx_key = v
            end
        end
    end

    if pfx_name == "" or pfx_key == "" then
        nauthilus_util.print_result(logging, logs, "Either pfx name or key is missing")

        return logs
    end

    local cache_value

    if pfx_name ~= "quota_flag" then
        cache_value = cache_load(pfx_key, pfx_name)
    end

    if cache_value then
        logs.result = cache_value
        logs.cached = true
    else
        local response = handle_name(pfx_name, pfx_key, session, logging)
        if response then
            if pfx_name ~= "quota_flag" then
                cache_save(pfx_key, pfx_name, response, REDIS_KEY_EXPIRE)
            end

            logs.cached = false
            logs.result = response
        end
    end

    nauthilus_util.print_result(logging, logs)

    return logs
end

And finally a template for policies:

local nauthilus_util = require("nauthilus_util")

dynamic_loader("nauthilus_http_request")
local nauthilus_http_request = require("nauthilus_http_request")

dynamic_loader("nauthilus_gll_json")
local json = require("json")

local N = "pfxpolicy"

local unpack = table.unpack or unpack
local logs = {}

function nauthilus_run_hook(logging, session)
    logs.level = "info"
    logs.caller = N .. ".lua"
    logs.session = session

    local header = nauthilus_http_request.get_http_request_header("Content-Type")
    local body = nauthilus_http_request.get_http_request_body()

    if nauthilus_util.table_length(header) == 0 or header[1] ~= "application/json" then
        nauthilus_util.print_result(logging, logs, "HTTP request header: Wrong 'Content-Type'")

        return logs
    end

    local body_table, err_jdec = json.decode(body)
    nauthilus_util.if_error_raise(err_jdec)

    local postfix_request = ""

    for k,v in pairs(body_table) do
        local space = ", "

        if postfix_request == "" then
            space = ""
        end

        postfix_request = postfix_request .. space .. k .. "=" .. v
    end

    logs.postfix_request = postfix_request
    logs.result = "DUNNO"

    nauthilus_util.print_result(logging, logs)

    return logs
end

Clone this wiki locally