From 15776790533dd2b1b5742c0349106a24dda2766a Mon Sep 17 00:00:00 2001 From: irl Date: Sat, 3 May 2025 15:19:47 +0100 Subject: [PATCH] feat: adds module to obtain registered domain name using PSL --- src/lua/psl.lua | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/lua/psl.lua diff --git a/src/lua/psl.lua b/src/lua/psl.lua new file mode 100644 index 0000000..6ce88ba --- /dev/null +++ b/src/lua/psl.lua @@ -0,0 +1,105 @@ +local http = require "resty.http" + +local _M = {} + +local function fetch_psl() + local httpc = http.new() + local res, err = httpc:request_uri("https://publicsuffix.org/list/public_suffix_list.dat", { + method = "GET", + ssl_verify = true + }) + + if not res then + return nil, "Failed to fetch PSL: " .. err + end + + local rules = {} + for line in res.body:gmatch("[^\r\n]+") do + line = line:match("^%s*(.-)%s*$") + if line ~= "" and not line:match("^//") then + table.insert(rules, line) + end + end + return rules +end + +local function load_psl() + local cache = ngx.shared.jasima_cache + local cached = cache:get("psl") + if cached then return cached end + local rules, err = fetch_psl() + if rules then + cache:set("psl", rules, 86400 * 7) -- 1 week + return rules + else + ngx.log(ngx.ERR, err or "Unknown error loading PSL") + return nil + end +end + +local function split(domain) + local parts = {} + for part in domain:gmatch("[^%.]+") do table.insert(parts, part) end + return parts +end + +local function domain_matches_rule(domain_parts, rule_parts) + for i = 1, #rule_parts do + local rule_part = rule_parts[#rule_parts - i + 1] + local domain_part = domain_parts[#domain_parts - i + 1] + if rule_part == "*" then + -- wildcard match + elseif not domain_part or rule_part ~= domain_part then + return false + end + end + return true +end + +local function find_matching_rule(domain, rules) + local domain_parts = split(domain) + local match = nil + local max_len = 0 + + for _, rule in ipairs(rules) do + local is_exception = rule:sub(1, 1) == "!" + local clean_rule = is_exception and rule:sub(2) or rule + local rule_parts = split(clean_rule) + + if domain_matches_rule(domain_parts, rule_parts) then + if #rule_parts > max_len then + match = { rule = rule, is_exception = is_exception } + max_len = #rule_parts + end + end + end + + return match +end + +function _M.get_registered_domain(domain) + if not domain or domain == "" then return nil end + local rules = load_psl() + if not rules then return nil end + + local domain_parts = split(domain:lower()) + local match = find_matching_rule(domain, rules) + if not match then return domain end + + local rule = match.rule + local is_exception = match.is_exception + local rule_parts = split(rule:gsub("^!", "")) + local offset = is_exception and (#rule_parts + 1) or #rule_parts + + local start_idx = #domain_parts - offset + if start_idx < 1 then return domain end + + local reg_parts = {} + for i = start_idx, #domain_parts do + table.insert(reg_parts, domain_parts[i]) + end + + return table.concat(reg_parts, ".") +end + +return _M