local rules = require "kong.plugins.bot-detection.rules" local strip = require("kong.tools.string").strip local lrucache = require "resty.lrucache" local kong_meta = require "kong.meta" local ipairs = ipairs local re_find = ngx.re.find local BotDetectionHandler = {} BotDetectionHandler.PRIORITY = 2500 BotDetectionHandler.VERSION = kong_meta.version local BAD_REQUEST = 400 local FORBIDDEN = 403 local MATCH_EMPTY = 0 local MATCH_ALLOW = 1 local MATCH_DENY = 2 local MATCH_BOT = 3 -- per-worker cache of matched UAs -- we use a weak table, index by the `conf` parameter, so once the plugin config -- is GC'ed, the cache follows automatically local ua_caches = setmetatable({}, { __mode = "k" }) local UA_CACHE_SIZE = 10 ^ 4 local function get_user_agent() local user_agent = kong.request.get_headers()["user-agent"] if type(user_agent) == "table" then return nil, "Only one User-Agent header allowed" end return user_agent end local function examine_agent(user_agent, conf) user_agent = strip(user_agent) if conf.allow then for _, rule in ipairs(conf.allow) do if re_find(user_agent, rule, "jo") then return MATCH_ALLOW end end end if conf.deny then for _, rule in ipairs(conf.deny) do if re_find(user_agent, rule, "jo") then return MATCH_DENY end end end for _, rule in ipairs(rules.bots) do if re_find(user_agent, rule, "jo") then return MATCH_BOT end end return MATCH_EMPTY end function BotDetectionHandler:access(conf) local user_agent, err = get_user_agent() if err then return kong.response.error(BAD_REQUEST, err) end if not user_agent then return end local cache = ua_caches[conf] if not cache then cache = lrucache.new(UA_CACHE_SIZE) ua_caches[conf] = cache end local match = cache:get(user_agent) if not match then match = examine_agent(user_agent, conf) cache:set(user_agent, match) end -- if we saw a denied UA or bot, return forbidden. otherwise, -- fall out of our handler if match > 1 then return kong.response.error(FORBIDDEN) end end return BotDetectionHandler