if ($http_user_agent~* (curl|wget|httpie|python|Go-http-client|okhttp|java|bot|spider|crawler|Postman|Apache-HttpClient|HeadlessChrome|xx032_)) { set$block_request1; }
if ($http_user_agent~* "UptimeRobot") { set$block_request0; }
if ($block_request = 1) { return403; }
###### 非浏览器请求拦截 ######
###### Artalk评论昵称关键字拦截 ######
access_by_lua_block { -- 返回JSON错误 local function respond_error(status, message) ngx.status = status ngx.header["Content-Type"] = "application/json; charset=utf-8"
-- 跨域部署时允许Artalk前端读取错误信息 local origin = ngx.var.http_origin
if origin and origin ~= "" then ngx.header["Access-Control-Allow-Origin"] = origin ngx.header["Access-Control-Allow-Credentials"] = "true" ngx.header["Vary"] = "Origin" end
ngx.say('{"msg":"' .. message .. '"}') return ngx.exit(status) end
-- 只处理Artalk评论提交请求 if ngx.req.get_method() ~= "POST" or ( ngx.var.uri ~= "/api/v2/comments" and ngx.var.uri ~= "/api/v2/comments/" ) then return end
ngx.req.read_body()
local body = ngx.req.get_body_data()
-- 请求体较大时,Nginx可能将其写入临时文件 if not body then local body_file = ngx.req.get_body_file()
if body_file then local request_file, request_file_err = io.open(body_file, "rb")
if request_file then body = request_file:read("*a") request_file:close() else ngx.log( ngx.ERR, "Failed to read Artalk request body file: ", request_file_err or "unknown error", ", path: ", body_file ) end end end
if not body then ngx.log( ngx.ERR, "Failed to read Artalk request body" )
return respond_error( ngx.HTTP_INTERNAL_SERVER_ERROR, "Comment filtering is temporarily unavailable. Please try again later." ) end
local cjson = require "cjson.safe" local data, json_err = cjson.decode(body)
if type(data) ~= "table" then ngx.log( ngx.WARN, "Failed to parse Artalk request JSON: ", json_err or "unknown error" ) return end
if type(data.name) ~= "string" then return end
-- 统一处理昵称和屏蔽词 local function normalize(value) value = value:lower() value = value:gsub("%s+", "") value = value:gsub("[%.%-%_|]+", "") value = value:gsub("·", "") value = value:gsub("|", "") return value end
local name = normalize(data.name)
-- 尝试从常见OpenResty配置目录读取词库 local prefix = ngx.config.prefix() local separator = prefix:sub(-1) == "/" and "" or "/"
local candidate_paths = { prefix .. separator .. "conf.d/artalk-blocked-words.txt",
local words_file = nil local words_path = nil local words_file_err = nil
for _, candidate_path in ipairs(candidate_paths) do local candidate_file, candidate_err = io.open(candidate_path, "r")
if candidate_file then words_file = candidate_file words_path = candidate_path break end
words_file_err = candidate_err end
-- 读取失败时拒绝评论,防止过滤器静默失效 if not words_file then ngx.log( ngx.ERR, "Failed to open Artalk blocked words file: ", words_file_err or "unknown error", ", attempted paths: ", table.concat(candidate_paths, ", ") )
return respond_error( ngx.HTTP_INTERNAL_SERVER_ERROR, "Comment filtering is temporarily unavailable. Please try again later." ) end
local blocked_word = nil local loaded_word_count = 0
for line in words_file:lines() do -- 去除UTF-8 BOM line = line:gsub("^\239\187\191", "")
-- 去除行首和行尾空白 local word = line:match("^%s*(.-)%s*$")
-- 忽略空行和注释行 if word ~= "" and word:sub(1, 1) ~= "#" then word = normalize(word)
if word ~= "" then loaded_word_count = loaded_word_count + 1
if name:find(word, 1, true) then blocked_word = word break end end end end
words_file:close()
-- 词库为空时拒绝评论,防止过滤器静默失效 if loaded_word_count == 0 then ngx.log( ngx.ERR, "Artalk blocked words file is empty: ", words_path )
return respond_error( ngx.HTTP_INTERNAL_SERVER_ERROR, "Comment filtering is temporarily unavailable. Please try again later." ) end
if blocked_word then ngx.log( ngx.WARN, "Blocked Artalk spam nickname from ", ngx.var.remote_addr, ", matched word: ", blocked_word )
return respond_error( ngx.HTTP_FORBIDDEN, "Comment rejected: the nickname contains prohibited advertising or spam-related terms. Please use a different nickname." ) end }