-- Copyright 2025 WebPros International, LLC -- All rights reserved. -- copyright@cpanel.net http://cpanel.net -- This code is subject to the cPanel license. Unauthorized copying is prohibited. -- NOTE: Much of this code was originally generated via Augment Code's LLM product. -- We wound up doing some manual alteration to get this to actually work, but -- for the most part you can consider this code "generated" for purposes of -- auditing LLM usage within this product. -- This script replaces the dict-based authentication for Dovecot 2.4 -- It communicates with cPanel's authentication daemons via Unix sockets local socket = require("socket") local unix = require("socket.unix") local SOCKFILE = nil function script_init(args) SOCKFILE = args["socket"] return 0 end -- Simple JSON parser for cpauthd response -- This is a minimal implementation that handles the specific JSON format returned by cpauthd local function parse_json_simple(json_str) if not json_str or json_str == "" then return nil end -- Remove leading/trailing whitespace and braces json_str = json_str:match("^%s*{(.+)}%s*$") if not json_str then return nil end local result = {} local i = 1 local len = #json_str while i <= len do -- Skip whitespace and commas while i <= len and json_str:sub(i, i):match("[%s,]") do i = i + 1 end if i > len then break end -- Parse key if json_str:sub(i, i) ~= '"' then break end i = i + 1 local key_start = i while i <= len and json_str:sub(i, i) ~= '"' do i = i + 1 end if i > len then break end local key = json_str:sub(key_start, i - 1) i = i + 1 -- Skip whitespace and colon while i <= len and json_str:sub(i, i):match("[%s:]") do i = i + 1 end if i > len then break end -- Parse value local value if json_str:sub(i, i) == '"' then -- String value i = i + 1 local value_start = i while i <= len and json_str:sub(i, i) ~= '"' do i = i + 1 end if i > len then break end value = json_str:sub(value_start, i - 1) i = i + 1 else -- Non-string value (number, boolean, null) local value_start = i while i <= len and not json_str:sub(i, i):match("[,}]") do i = i + 1 end value = json_str:sub(value_start, i - 1):match("^%s*(.-)%s*$") -- Convert to appropriate type if value == "null" then value = nil elseif value == "true" then value = true elseif value == "false" then value = false elseif value:match("^%-?%d+%.?%d*$") then value = tonumber(value) end end result[key] = value end return result end -- Helper function to communicate with Unix socket local function query_socket(socket_path, query) local sock = unix() if not sock then return nil, "Failed to create socket" end local ok, err = sock:connect(socket_path) if not ok then sock:close() return nil, "Failed to connect to " .. socket_path .. ": " .. (err or "unknown error") end -- Send query local sent, err = sock:send(query .. "\n") if not sent then sock:close() return nil, "Failed to send query: " .. (err or "unknown error") end -- Read response local response, err = sock:receive("*l") sock:close() if not response then return nil, "Failed to receive response: " .. (err or "unknown error") end return response end -- Parse cpauthd response local function parse_cpauthd_response(response) if not response then return nil end -- Check if response starts with "O" (success) and contains JSON if response:sub(1, 1) == "O" and response:sub(2, 2) == "{" and response:sub(-1) == "}" then -- Extract JSON part (remove "O" prefix) local json_data = response:sub(2) -- Parse JSON using our simple parser local fields = parse_json_simple(json_data) if fields and type(fields) == "table" then return fields else return nil end end -- Anything else is invalid/not found, so just return nil. return nil end -- Main passdb lookup function function auth_passdb_lookup(req) local protocol = req.protocol or "imap" local namespace = req.namespace or "shared" local user = req.user if not user then return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No username provided" end -- Query format: L + namespace + /dovecot_userdb- + protocol + / + username local query = "L" .. namespace .. "/dovecot_userdb-" .. protocol .. "/" .. user local response, err = query_socket(SOCKFILE, query) if not response then return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Socket error: " .. (err or "unknown") end local fields = parse_cpauthd_response(response) if not fields then return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "User not found" end -- Return success with the parsed fields return dovecot.auth.PASSDB_RESULT_OK, fields end -- Irritatingly, you can't just alias the function even though it's basically -- the same thing. function auth_userdb_lookup(req) local protocol = req.protocol or "imap" local namespace = req.namespace or "shared" local user = req.user if not user then return dovecot.auth.USERDB_RESULT_USER_UNKNOWN, "No username provided" end -- Query format: L + namespace + /dovecot_userdb- + protocol + / + username local query = "L" .. namespace .. "/dovecot_userdb-" .. protocol .. "/" .. user local response, err = query_socket(SOCKFILE, query) if not response then return dovecot.auth.USERDB_RESULT_INTERNAL_FAILURE, "Socket error: " .. (err or "unknown") end local fields = parse_cpauthd_response(response) if not fields then return dovecot.auth.USERDB_RESULT_USER_UNKNOWN, "User not found" end -- Return success with the parsed fields return dovecot.auth.USERDB_RESULT_OK, fields end function auth_passdb_get_cache_key() return "dovecot_userdb-%{user|username}/%{protocol}" end function auth_userdb_get_cache_key() return "dovecot_userdb-%{user|username}/%{protocol}" end