AppSec Component integration (#43)
Integrate AppSec Component --------- Co-authored-by: Sebastien Blot <sebastien@crowdsec.net>
This commit is contained in:
parent
0461b74b22
commit
926de93ce2
6 changed files with 805 additions and 88 deletions
188
lib/crowdsec.lua
188
lib/crowdsec.lua
|
@ -5,8 +5,12 @@ local iputils = require "plugins.crowdsec.iputils"
|
|||
local http = require "resty.http"
|
||||
local cjson = require "cjson"
|
||||
local captcha = require "plugins.crowdsec.captcha"
|
||||
local flag = require "plugins.crowdsec.flag"
|
||||
local utils = require "plugins.crowdsec.utils"
|
||||
local ban = require "plugins.crowdsec.ban"
|
||||
local url = require "plugins.crowdsec.url"
|
||||
local bit
|
||||
if _VERSION == "Lua 5.1" then bit = require "bit" else bit = require "bit32" end
|
||||
|
||||
-- contain runtime = {}
|
||||
local runtime = {}
|
||||
|
@ -21,6 +25,11 @@ runtime.timer_started = false
|
|||
|
||||
local csmod = {}
|
||||
|
||||
local PASSTHROUGH = "passthrough"
|
||||
local DENY = "deny"
|
||||
|
||||
local APPSEC_API_KEY_HEADER = "x-crowdsec-appsec-api-key"
|
||||
local REMEDIATION_API_KEY_HEADER = 'x-api-key'
|
||||
|
||||
|
||||
-- init function
|
||||
|
@ -66,6 +75,30 @@ function csmod.init(configFile, userAgent)
|
|||
table.insert(runtime.conf["EXCLUDE_LOCATION"], runtime.conf["REDIRECT_LOCATION"])
|
||||
end
|
||||
|
||||
if runtime.conf["SSL_VERIFY"] == "false" then
|
||||
runtime.conf["SSL_VERIFY"] = false
|
||||
else
|
||||
runtime.conf["SSL_VERIFY"] = true
|
||||
end
|
||||
|
||||
if runtime.conf["ALWAYS_SEND_TO_APPSEC"] == "false" then
|
||||
runtime.conf["ALWAYS_SEND_TO_APPSEC"] = false
|
||||
else
|
||||
runtime.conf["ALWAYS_SEND_TO_APPSEC"] = true
|
||||
end
|
||||
|
||||
runtime.conf["APPSEC_ENABLED"] = false
|
||||
|
||||
if runtime.conf["APPSEC_URL"] ~= "" then
|
||||
u = url.parse(runtime.conf["APPSEC_URL"])
|
||||
runtime.conf["APPSEC_ENABLED"] = true
|
||||
runtime.conf["APPSEC_HOST"] = u.host
|
||||
if u.port ~= nil then
|
||||
runtime.conf["APPSEC_HOST"] = runtime.conf["APPSEC_HOST"] .. ":" .. u.port
|
||||
end
|
||||
ngx.log(ngx.ERR, "APPSEC is enabled on '" .. runtime.conf["APPSEC_HOST"] .. "'")
|
||||
end
|
||||
|
||||
|
||||
-- if stream mode, add callback to stream_query and start timer
|
||||
if runtime.conf["MODE"] == "stream" then
|
||||
|
@ -85,6 +118,8 @@ function csmod.init(configFile, userAgent)
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
|
@ -94,16 +129,17 @@ function csmod.validateCaptcha(captcha_res, remote_ip)
|
|||
end
|
||||
|
||||
|
||||
local function get_http_request(link)
|
||||
local function get_remediation_http_request(link)
|
||||
local httpc = http.new()
|
||||
httpc:set_timeout(runtime.conf['REQUEST_TIMEOUT'])
|
||||
local res, err = httpc:request_uri(link, {
|
||||
method = "GET",
|
||||
headers = {
|
||||
['Connection'] = 'close',
|
||||
['X-Api-Key'] = runtime.conf["API_KEY"],
|
||||
[REMEDIATION_API_KEY_HEADER] = runtime.conf["API_KEY"],
|
||||
['User-Agent'] = runtime.userAgent
|
||||
},
|
||||
ssl_verify = runtime.conf["SSL_VERIFY"]
|
||||
})
|
||||
httpc:close()
|
||||
return res, err
|
||||
|
@ -226,7 +262,7 @@ local function stream_query(premature)
|
|||
local is_startup = runtime.cache:get("startup")
|
||||
ngx.log(ngx.DEBUG, "Stream Query from worker : " .. tostring(ngx.worker.id()) .. " with startup "..tostring(is_startup) .. " | premature: " .. tostring(premature))
|
||||
local link = runtime.conf["API_URL"] .. "/v1/decisions/stream?startup=" .. tostring(is_startup)
|
||||
local res, err = get_http_request(link)
|
||||
local res, err = get_remediation_http_request(link)
|
||||
if not res then
|
||||
local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query)
|
||||
if not ok then
|
||||
|
@ -321,7 +357,7 @@ end
|
|||
|
||||
local function live_query(ip)
|
||||
local link = runtime.conf["API_URL"] .. "/v1/decisions?ip=" .. ip
|
||||
local res, err = get_http_request(link)
|
||||
local res, err = get_remediation_http_request(link)
|
||||
if not res then
|
||||
return true, nil, "request failed: ".. err
|
||||
end
|
||||
|
@ -365,6 +401,22 @@ local function live_query(ip)
|
|||
end
|
||||
end
|
||||
|
||||
local function get_body()
|
||||
|
||||
ngx.req.read_body()
|
||||
local body = ngx.req.get_body_data()
|
||||
if body == nil then
|
||||
local bodyfile = ngx.req.get_body_file()
|
||||
if bodyfile then
|
||||
local fh, err = io.open(bodyfile, "r")
|
||||
if fh then
|
||||
body = fh:read("*a")
|
||||
fh:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
return body
|
||||
end
|
||||
|
||||
function csmod.GetCaptchaTemplate()
|
||||
return captcha.GetTemplate()
|
||||
|
@ -438,12 +490,83 @@ function csmod.allowIp(ip)
|
|||
return true, nil, nil
|
||||
end
|
||||
|
||||
|
||||
function csmod.AppSecCheck()
|
||||
local httpc = http.new()
|
||||
httpc:set_timeouts(runtime.conf["APPSEC_CONNECT_TIMEOUT"], runtime.conf["APPSEC_SEND_TIMEOUT"], runtime.conf["APPSEC_PROCESS_TIMEOUT"])
|
||||
|
||||
local uri = ngx.var.request_uri
|
||||
local headers = ngx.req.get_headers()
|
||||
|
||||
-- overwrite headers with crowdsec appsec require headers
|
||||
headers["x-crowdsec-appsec-ip"] = ngx.var.remote_addr
|
||||
headers["x-crowdsec-appsec-host"] = ngx.var.http_host
|
||||
headers["x-crowdsec-appsec-verb"] = ngx.var.request_method
|
||||
headers["x-crowdsec-appsec-uri"] = uri
|
||||
headers[APPSEC_API_KEY_HEADER] = runtime.conf["API_KEY"]
|
||||
|
||||
-- set CrowdSec APPSEC Host
|
||||
headers["host"] = runtime.conf["APPSEC_HOST"]
|
||||
|
||||
local ok, remediation = true, "allow"
|
||||
if runtime.conf["APPSEC_FAILURE_ACTION"] == DENY then
|
||||
ok = false
|
||||
remediation = runtime.conf["FALLBACK_REMEDIATION"]
|
||||
end
|
||||
|
||||
local method = "GET"
|
||||
|
||||
local body = get_body()
|
||||
if body ~= nil then
|
||||
if #body > 0 then
|
||||
method = "POST"
|
||||
if headers["content-length"] == nil then
|
||||
headers["content-length"] = tostring(#body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local res, err = httpc:request_uri(runtime.conf["APPSEC_URL"], {
|
||||
method = method,
|
||||
headers = headers,
|
||||
body = body,
|
||||
ssl_verify = runtime.conf["SSL_VERIFY"],
|
||||
})
|
||||
httpc:close()
|
||||
|
||||
if err ~= nil then
|
||||
ngx.log(ngx.ERR, "Fallback because of err: " .. err)
|
||||
return ok, remediation, err
|
||||
end
|
||||
|
||||
if res.status == 200 then
|
||||
ok = true
|
||||
remediation = "allow"
|
||||
elseif res.status == 403 then
|
||||
ok = false
|
||||
local response = cjson.decode(res.body)
|
||||
remediation = response.action
|
||||
elseif res.status == 401 then
|
||||
ngx.log(ngx.ERR, "Unauthenticated request to APPSEC")
|
||||
else
|
||||
ngx.log(ngx.ERR, "Bad request to APPSEC (" .. res.status .. "): " .. res.body)
|
||||
end
|
||||
|
||||
return ok, remediation, err
|
||||
|
||||
end
|
||||
|
||||
function csmod.Allow(ip)
|
||||
|
||||
if runtime.conf["ENABLED"] == "false" then
|
||||
return "Disabled", nil
|
||||
end
|
||||
|
||||
if ngx.req.is_internal() then
|
||||
return
|
||||
end
|
||||
|
||||
local remediationSource = flag.BOUNCER_SOURCE
|
||||
|
||||
if utils.table_len(runtime.conf["EXCLUDE_LOCATION"]) > 0 then
|
||||
for k, v in pairs(runtime.conf["EXCLUDE_LOCATION"]) do
|
||||
if ngx.var.uri == v then
|
||||
|
@ -470,6 +593,23 @@ function csmod.Allow(ip)
|
|||
ngx.shared.crowdsec_cache:delete("captcha_" .. ip)
|
||||
end
|
||||
|
||||
-- check with appSec if the remediation component doesn't have decisions for the IP
|
||||
-- OR
|
||||
-- that user configured the remediation component to always check on the appSec (even if there is a decision for the IP)
|
||||
if ok == true or runtime.conf["ALWAYS_SEND_TO_APPSEC"] == true then
|
||||
if runtime.conf["APPSEC_ENABLED"] == true and ngx.var.no_appsec ~= "1" then
|
||||
local appsecOk, appsecRemediation, err = csmod.AppSecCheck()
|
||||
if err ~= nil then
|
||||
ngx.log(ngx.ERR, "AppSec check: " .. err)
|
||||
end
|
||||
if appsecOk == false then
|
||||
ok = false
|
||||
remediationSource = flag.APPSEC_SOURCE
|
||||
remediation = appsecRemediation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local captcha_ok = runtime.cache:get("captcha_ok")
|
||||
|
||||
if runtime.fallback ~= "" then
|
||||
|
@ -486,8 +626,9 @@ function csmod.Allow(ip)
|
|||
|
||||
if captcha_ok then -- if captcha can be use (configuration is valid)
|
||||
-- we check if the IP need to validate its captcha before checking it against crowdsec local API
|
||||
local previous_uri, state_id = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr)
|
||||
if previous_uri ~= nil and state_id == captcha.GetStateID(captcha._VERIFY_STATE) then
|
||||
local previous_uri, flags = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr)
|
||||
local source, state_id, err = flag.GetFlags(flags)
|
||||
if previous_uri ~= nil and state_id == flag.VERIFY_STATE then
|
||||
ngx.req.read_body()
|
||||
local captcha_res = ngx.req.get_post_args()[csmod.GetCaptchaBackendKey()] or 0
|
||||
if captcha_res ~= 0 then
|
||||
|
@ -496,15 +637,22 @@ function csmod.Allow(ip)
|
|||
ngx.log(ngx.ERR, "Error while validating captcha: " .. err)
|
||||
end
|
||||
if valid == true then
|
||||
-- if the captcha is valid and has been applied by the application security component
|
||||
-- then we delete the state from the cache because from the bouncing part, if the user solve the captcha
|
||||
-- we will not propose a captcha until the 'CAPTCHA_EXPIRATION'.
|
||||
-- But for the Application security component, we serve the captcha each time the user trigger it.
|
||||
if source == flag.APPSEC_SOURCE then
|
||||
ngx.shared.crowdsec_cache:delete("captcha_"..ngx.var.remote_addr)
|
||||
else
|
||||
local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, previous_uri, runtime.conf["CAPTCHA_EXPIRATION"], bit.bor(flag.VALIDATED_STATE, source) )
|
||||
if not succ then
|
||||
ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err)
|
||||
end
|
||||
if forcible then
|
||||
ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config")
|
||||
end
|
||||
end
|
||||
-- captcha is valid, we redirect the IP to its previous URI but in GET method
|
||||
local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, previous_uri, runtime.conf["CAPTCHA_EXPIRATION"], captcha.GetStateID(captcha._VALIDATED_STATE))
|
||||
if not succ then
|
||||
ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err)
|
||||
end
|
||||
if forcible then
|
||||
ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config")
|
||||
end
|
||||
|
||||
ngx.req.set_method(ngx.HTTP_GET)
|
||||
ngx.redirect(previous_uri)
|
||||
return
|
||||
|
@ -514,18 +662,18 @@ function csmod.Allow(ip)
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not ok then
|
||||
if remediation == "ban" then
|
||||
ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ngx.var.remote_addr .. "' with '"..remediation.."'")
|
||||
ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ngx.var.remote_addr .. "' with '"..remediation.."' (by " .. flag.Flags[remediationSource] .. ")")
|
||||
ban.apply()
|
||||
return
|
||||
end
|
||||
-- if the remediation is a captcha and captcha is well configured
|
||||
if remediation == "captcha" and captcha_ok and ngx.var.uri ~= "/favicon.ico" then
|
||||
local previous_uri, state_id = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr)
|
||||
local previous_uri, flags = ngx.shared.crowdsec_cache:get("captcha_"..ngx.var.remote_addr)
|
||||
source, state_id, err = flag.GetFlags(flags)
|
||||
-- we check if the IP is already in cache for captcha and not yet validated
|
||||
if previous_uri == nil or state_id ~= captcha.GetStateID(captcha._VALIDATED_STATE) then
|
||||
if previous_uri == nil or remediationSource == flag.APPSEC_SOURCE then
|
||||
ngx.header.content_type = "text/html"
|
||||
ngx.header.cache_control = "no-cache"
|
||||
ngx.say(csmod.GetCaptchaTemplate())
|
||||
|
@ -539,7 +687,7 @@ function csmod.Allow(ip)
|
|||
end
|
||||
end
|
||||
end
|
||||
local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, uri , 60, captcha.GetStateID(captcha._VERIFY_STATE))
|
||||
local succ, err, forcible = ngx.shared.crowdsec_cache:set("captcha_"..ngx.var.remote_addr, uri , 60, bit.bor(flag.VERIFY_STATE, remediationSource))
|
||||
if not succ then
|
||||
ngx.log(ngx.ERR, "failed to add key about captcha for ip '" .. ngx.var.remote_addr .. "' in cache: "..err)
|
||||
end
|
||||
|
|
|
@ -3,7 +3,6 @@ local cjson = require "cjson"
|
|||
local template = require "plugins.crowdsec.template"
|
||||
local utils = require "plugins.crowdsec.utils"
|
||||
|
||||
|
||||
local M = {_TYPE='module', _NAME='recaptcha.funcs', _VERSION='1.0-0'}
|
||||
|
||||
local captcha_backend_url = {}
|
||||
|
@ -21,30 +20,10 @@ captcha_frontend_key["recaptcha"] = "g-recaptcha"
|
|||
captcha_frontend_key["hcaptcha"] = "h-captcha"
|
||||
captcha_frontend_key["turnstile"] = "cf-turnstile"
|
||||
|
||||
M._VERIFY_STATE = "to_verify"
|
||||
M._VALIDATED_STATE = "validated"
|
||||
|
||||
|
||||
M.State = {}
|
||||
M.State["1"] = M._VERIFY_STATE
|
||||
M.State["2"] = M._VALIDATED_STATE
|
||||
|
||||
M.SecretKey = ""
|
||||
M.SiteKey = ""
|
||||
M.Template = ""
|
||||
|
||||
|
||||
function M.GetStateID(state)
|
||||
for k, v in pairs(M.State) do
|
||||
if v == state then
|
||||
return tonumber(k)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
function M.New(siteKey, secretKey, TemplateFilePath, captcha_provider)
|
||||
|
||||
if siteKey == nil or siteKey == "" then
|
||||
|
|
|
@ -39,8 +39,8 @@ function config.loadConfig(file)
|
|||
return nil, "File ".. file .." doesn't exist"
|
||||
end
|
||||
local conf = {}
|
||||
local valid_params = {'ENABLED','API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION', 'CAPTCHA_PROVIDER'}
|
||||
local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY', 'CAPTCHA_EXPIRATION'}
|
||||
local valid_params = {'ENABLED','API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE', 'SECRET_KEY', 'SITE_KEY', 'BAN_TEMPLATE_PATH' ,'CAPTCHA_TEMPLATE_PATH', 'REDIRECT_LOCATION', 'RET_CODE', 'EXCLUDE_LOCATION', 'FALLBACK_REMEDIATION', 'CAPTCHA_PROVIDER', 'APPSEC_URL', 'APPSEC_FAILURE_ACTION', 'ALWAYS_SEND_TO_APPSEC', 'SSL_VERIFY'}
|
||||
local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY', 'CAPTCHA_EXPIRATION', 'APPSEC_CONNECT_TIMEOUT', 'APPSEC_SEND_TIMEOUT', 'APPSEC_PROCESS_TIMEOUT'}
|
||||
local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'}
|
||||
local valid_truefalse_values = {'false', 'true'}
|
||||
local default_values = {
|
||||
|
@ -53,7 +53,15 @@ function config.loadConfig(file)
|
|||
['REDIRECT_LOCATION'] = "",
|
||||
['EXCLUDE_LOCATION'] = {},
|
||||
['RET_CODE'] = 0,
|
||||
['CAPTCHA_PROVIDER'] = "recaptcha"
|
||||
['CAPTCHA_PROVIDER'] = "recaptcha",
|
||||
['APPSEC_URL'] = "",
|
||||
['APPSEC_CONNECT_TIMEOUT'] = 100,
|
||||
['APPSEC_SEND_TIMEOUT'] = 100,
|
||||
['APPSEC_PROCESS_TIMEOUT'] = 500,
|
||||
['APPSEC_FAILURE_ACTION'] = "passthrough",
|
||||
['SSL_VERIFY'] = "true",
|
||||
['ALWAYS_SEND_TO_APPSEC'] = "false",
|
||||
|
||||
}
|
||||
for line in io.lines(file) do
|
||||
local isOk = false
|
||||
|
@ -64,51 +72,57 @@ function config.loadConfig(file)
|
|||
isOk = true
|
||||
end
|
||||
if not isOk then
|
||||
local sep_pos = line:find("=")
|
||||
if not sep_pos then
|
||||
ngx.log(ngx.ERR, "invalid configuration line: " .. line)
|
||||
break
|
||||
end
|
||||
local sep_pos = line:find("=")
|
||||
if not sep_pos then
|
||||
ngx.log(ngx.ERR, "invalid configuration line: " .. line)
|
||||
break
|
||||
end
|
||||
local key = trim(line:sub(1, sep_pos - 1))
|
||||
local value = trim(line:sub(sep_pos + 1))
|
||||
if has_value(valid_params, key) then
|
||||
if key == "ENABLED" then
|
||||
if not has_value(valid_truefalse_values, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'true' instead")
|
||||
conf[key] = "true"
|
||||
end
|
||||
elseif key == "BOUNCING_ON_TYPE" then
|
||||
if not has_value(valid_bouncing_on_type_values, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'ban' instead")
|
||||
conf[key] = "ban"
|
||||
end
|
||||
elseif key == "MODE" then
|
||||
if not has_value({'stream', 'live'}, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'stream' instead")
|
||||
conf[key] = "stream"
|
||||
end
|
||||
elseif key == "EXCLUDE_LOCATION" then
|
||||
exclude_location = {}
|
||||
if value ~= "" then
|
||||
for match in (value..","):gmatch("(.-)"..",") do
|
||||
table.insert(exclude_location, match)
|
||||
end
|
||||
end
|
||||
conf[key] = exclude_location
|
||||
elseif key == "FALLBACK_REMEDIATION" then
|
||||
if not has_value({'captcha', 'ban'}, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'ban' instead")
|
||||
conf[key] = "ban"
|
||||
end
|
||||
else
|
||||
conf[key] = value
|
||||
end
|
||||
elseif has_value(valid_int_params, key) then
|
||||
conf[key] = tonumber(value)
|
||||
else
|
||||
ngx.log(ngx.ERR, "unsupported configuration '" .. key .. "'")
|
||||
end
|
||||
end
|
||||
local value = trim(line:sub(sep_pos + 1))
|
||||
if has_value(valid_params, key) then
|
||||
if key == "ENABLED" then
|
||||
if not has_value(valid_truefalse_values, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'true' instead")
|
||||
value = "true"
|
||||
end
|
||||
elseif key == "BOUNCING_ON_TYPE" then
|
||||
if not has_value(valid_bouncing_on_type_values, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'ban' instead")
|
||||
value = "ban"
|
||||
end
|
||||
elseif key == "SSL_VERIFY" then
|
||||
if not has_value(valid_truefalse_values, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'true' instead")
|
||||
value = "true"
|
||||
end
|
||||
elseif key == "MODE" then
|
||||
if not has_value({'stream', 'live'}, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'stream' instead")
|
||||
value = "stream"
|
||||
end
|
||||
elseif key == "EXCLUDE_LOCATION" then
|
||||
exclude_location = {}
|
||||
if value ~= "" then
|
||||
for match in (value..","):gmatch("(.-)"..",") do
|
||||
table.insert(exclude_location, match)
|
||||
end
|
||||
end
|
||||
value = exclude_location
|
||||
elseif key == "FALLBACK_REMEDIATION" then
|
||||
if not has_value({'captcha', 'ban'}, value) then
|
||||
ngx.log(ngx.ERR, "unsupported value '" .. value .. "' for variable '" .. key .. "'. Using default value 'ban' instead")
|
||||
value = "ban"
|
||||
end
|
||||
end
|
||||
|
||||
conf[key] = value
|
||||
|
||||
elseif has_value(valid_int_params, key) then
|
||||
conf[key] = tonumber(value)
|
||||
else
|
||||
ngx.log(ngx.ERR, "unsupported configuration '" .. key .. "'")
|
||||
end
|
||||
end
|
||||
end
|
||||
for k, v in pairs(default_values) do
|
||||
if conf[k] == nil then
|
||||
|
|
52
lib/plugins/crowdsec/flag.lua
Normal file
52
lib/plugins/crowdsec/flag.lua
Normal file
|
@ -0,0 +1,52 @@
|
|||
local bit
|
||||
if _VERSION == "Lua 5.1" then bit = require "bit" else bit = require "bit32" end
|
||||
|
||||
local M = {_TYPE='module', _NAME='flag.funcs', _VERSION='1.0-0'}
|
||||
|
||||
M.BOUNCER_SOURCE = 0x1
|
||||
M.APPSEC_SOURCE = 0x2
|
||||
M.VERIFY_STATE = 0x4
|
||||
M.VALIDATED_STATE = 0x8
|
||||
|
||||
M.Flags = {}
|
||||
M.Flags[0x0] = ""
|
||||
M.Flags[0x1] = "bouncer"
|
||||
M.Flags[0x2] = "appsec"
|
||||
M.Flags[0x4] = "to_verify"
|
||||
M.Flags[0x8] = "validated"
|
||||
|
||||
|
||||
function M.GetFlags(flags)
|
||||
local source = 0x0
|
||||
local err = ""
|
||||
local state = 0x0
|
||||
|
||||
if flags == nil then
|
||||
return source, state, err
|
||||
end
|
||||
|
||||
if bit.band(flags, M.BOUNCER_SOURCE) then
|
||||
source = M.BOUNCER_SOURCE
|
||||
elseif bit.band(flags, M.APPSEC_SOURCE) then
|
||||
source = M.APPSEC_SOURCE
|
||||
end
|
||||
|
||||
if bit.band(flags, M.VERIFY_STATE) then
|
||||
state = M.VERIFY_STATE
|
||||
elseif bit.band(flags, M.VALIDATED_STATE) then
|
||||
state = M.VALIDATED_STATE
|
||||
end
|
||||
return source, state, err
|
||||
|
||||
end
|
||||
|
||||
function M.GetStateID(state)
|
||||
for k, v in pairs(M.State) do
|
||||
if v == state then
|
||||
return tonumber(k)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return M
|
523
lib/plugins/crowdsec/url.lua
Normal file
523
lib/plugins/crowdsec/url.lua
Normal file
|
@ -0,0 +1,523 @@
|
|||
-- net/url.lua - a robust url parser and builder
|
||||
--
|
||||
-- Bertrand Mansion, 2011-2021; License MIT
|
||||
-- @module net.url
|
||||
-- @alias M
|
||||
|
||||
local M = {}
|
||||
M.version = "1.1.0"
|
||||
|
||||
--- url options
|
||||
-- - `separator` is set to `&` by default but could be anything like `&amp;` or `;`
|
||||
-- - `cumulative_parameters` is false by default. If true, query parameters with the same name will be stored in a table.
|
||||
-- - `legal_in_path` is a table of characters that will not be url encoded in path components
|
||||
-- - `legal_in_query` is a table of characters that will not be url encoded in query values. Query parameters only support a small set of legal characters (-_.).
|
||||
-- - `query_plus_is_space` is true by default, so a plus sign in a query value will be converted to %20 (space), not %2B (plus)
|
||||
-- @todo Add option to limit the size of the argument table
|
||||
-- @todo Add option to limit the depth of the argument table
|
||||
-- @todo Add option to process dots in parameter names, ie. `param.filter=1`
|
||||
M.options = {
|
||||
separator = '&',
|
||||
cumulative_parameters = false,
|
||||
legal_in_path = {
|
||||
[":"] = true, ["-"] = true, ["_"] = true, ["."] = true,
|
||||
["!"] = true, ["~"] = true, ["*"] = true, ["'"] = true,
|
||||
["("] = true, [")"] = true, ["@"] = true, ["&"] = true,
|
||||
["="] = true, ["$"] = true, [","] = true,
|
||||
[";"] = true
|
||||
},
|
||||
legal_in_query = {
|
||||
[":"] = true, ["-"] = true, ["_"] = true, ["."] = true,
|
||||
[","] = true, ["!"] = true, ["~"] = true, ["*"] = true,
|
||||
["'"] = true, [";"] = true, ["("] = true, [")"] = true,
|
||||
["@"] = true, ["$"] = true,
|
||||
},
|
||||
query_plus_is_space = true
|
||||
}
|
||||
|
||||
--- list of known and common scheme ports
|
||||
-- as documented in <a href="http://www.iana.org/assignments/uri-schemes.html">IANA URI scheme list</a>
|
||||
M.services = {
|
||||
acap = 674,
|
||||
cap = 1026,
|
||||
dict = 2628,
|
||||
ftp = 21,
|
||||
gopher = 70,
|
||||
http = 80,
|
||||
https = 443,
|
||||
iax = 4569,
|
||||
icap = 1344,
|
||||
imap = 143,
|
||||
ipp = 631,
|
||||
ldap = 389,
|
||||
mtqp = 1038,
|
||||
mupdate = 3905,
|
||||
news = 2009,
|
||||
nfs = 2049,
|
||||
nntp = 119,
|
||||
rtsp = 554,
|
||||
sip = 5060,
|
||||
snmp = 161,
|
||||
telnet = 23,
|
||||
tftp = 69,
|
||||
vemmi = 575,
|
||||
afs = 1483,
|
||||
jms = 5673,
|
||||
rsync = 873,
|
||||
prospero = 191,
|
||||
videotex = 516
|
||||
}
|
||||
|
||||
local function decode(str)
|
||||
return (str:gsub("%%(%x%x)", function(c)
|
||||
return string.char(tonumber(c, 16))
|
||||
end))
|
||||
end
|
||||
|
||||
local function encode(str, legal)
|
||||
return (str:gsub("([^%w])", function(v)
|
||||
if legal[v] then
|
||||
return v
|
||||
end
|
||||
return string.upper(string.format("%%%02x", string.byte(v)))
|
||||
end))
|
||||
end
|
||||
|
||||
-- for query values, + can mean space if configured as such
|
||||
local function decodeValue(str)
|
||||
if M.options.query_plus_is_space then
|
||||
str = str:gsub('+', ' ')
|
||||
end
|
||||
return decode(str)
|
||||
end
|
||||
|
||||
local function concat(a, b)
|
||||
if type(a) == 'table' then
|
||||
return a:build() .. b
|
||||
else
|
||||
return a .. b:build()
|
||||
end
|
||||
end
|
||||
|
||||
function M:addSegment(path)
|
||||
if type(path) == 'string' then
|
||||
self.path = self.path .. '/' .. encode(path:gsub("^/+", ""), M.options.legal_in_path)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- builds the url
|
||||
-- @return a string representing the built url
|
||||
function M:build()
|
||||
local url = ''
|
||||
if self.path then
|
||||
local path = self.path
|
||||
url = url .. tostring(path)
|
||||
end
|
||||
if self.query then
|
||||
local qstring = tostring(self.query)
|
||||
if qstring ~= "" then
|
||||
url = url .. '?' .. qstring
|
||||
end
|
||||
end
|
||||
if self.host then
|
||||
local authority = self.host
|
||||
if self.port and self.scheme and M.services[self.scheme] ~= self.port then
|
||||
authority = authority .. ':' .. self.port
|
||||
end
|
||||
local userinfo
|
||||
if self.user and self.user ~= "" then
|
||||
userinfo = self.user
|
||||
if self.password then
|
||||
userinfo = userinfo .. ':' .. self.password
|
||||
end
|
||||
end
|
||||
if userinfo and userinfo ~= "" then
|
||||
authority = userinfo .. '@' .. authority
|
||||
end
|
||||
if authority then
|
||||
if url ~= "" then
|
||||
url = '//' .. authority .. '/' .. url:gsub('^/+', '')
|
||||
else
|
||||
url = '//' .. authority
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.scheme then
|
||||
url = self.scheme .. ':' .. url
|
||||
end
|
||||
if self.fragment then
|
||||
url = url .. '#' .. self.fragment
|
||||
end
|
||||
return url
|
||||
end
|
||||
|
||||
--- builds the querystring
|
||||
-- @param tab The key/value parameters
|
||||
-- @param sep The separator to use (optional)
|
||||
-- @param key The parent key if the value is multi-dimensional (optional)
|
||||
-- @return a string representing the built querystring
|
||||
function M.buildQuery(tab, sep, key)
|
||||
local query = {}
|
||||
if not sep then
|
||||
sep = M.options.separator or '&'
|
||||
end
|
||||
local keys = {}
|
||||
for k in pairs(tab) do
|
||||
keys[#keys+1] = k
|
||||
end
|
||||
table.sort(keys, function (a, b)
|
||||
local function padnum(n, rest) return ("%03d"..rest):format(tonumber(n)) end
|
||||
return tostring(a):gsub("(%d+)(%.)",padnum) < tostring(b):gsub("(%d+)(%.)",padnum)
|
||||
end)
|
||||
for _,name in ipairs(keys) do
|
||||
local value = tab[name]
|
||||
name = encode(tostring(name), {["-"] = true, ["_"] = true, ["."] = true})
|
||||
if key then
|
||||
if M.options.cumulative_parameters and string.find(name, '^%d+$') then
|
||||
name = tostring(key)
|
||||
else
|
||||
name = string.format('%s[%s]', tostring(key), tostring(name))
|
||||
end
|
||||
end
|
||||
if type(value) == 'table' then
|
||||
query[#query+1] = M.buildQuery(value, sep, name)
|
||||
else
|
||||
local value = encode(tostring(value), M.options.legal_in_query)
|
||||
if value ~= "" then
|
||||
query[#query+1] = string.format('%s=%s', name, value)
|
||||
else
|
||||
query[#query+1] = name
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(query, sep)
|
||||
end
|
||||
|
||||
--- Parses the querystring to a table
|
||||
-- This function can parse multidimensional pairs and is mostly compatible
|
||||
-- with PHP usage of brackets in key names like ?param[key]=value
|
||||
-- @param str The querystring to parse
|
||||
-- @param sep The separator between key/value pairs, defaults to `&`
|
||||
-- @todo limit the max number of parameters with M.options.max_parameters
|
||||
-- @return a table representing the query key/value pairs
|
||||
function M.parseQuery(str, sep)
|
||||
if not sep then
|
||||
sep = M.options.separator or '&'
|
||||
end
|
||||
|
||||
local values = {}
|
||||
for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do
|
||||
local key = decodeValue(key)
|
||||
local keys = {}
|
||||
key = key:gsub('%[([^%]]*)%]', function(v)
|
||||
-- extract keys between balanced brackets
|
||||
if string.find(v, "^-?%d+$") then
|
||||
v = tonumber(v)
|
||||
else
|
||||
v = decodeValue(v)
|
||||
end
|
||||
table.insert(keys, v)
|
||||
return "="
|
||||
end)
|
||||
key = key:gsub('=+.*$', "")
|
||||
key = key:gsub('%s', "_") -- remove spaces in parameter name
|
||||
val = val:gsub('^=+', "")
|
||||
|
||||
if not values[key] then
|
||||
values[key] = {}
|
||||
end
|
||||
if #keys > 0 and type(values[key]) ~= 'table' then
|
||||
values[key] = {}
|
||||
elseif #keys == 0 and type(values[key]) == 'table' then
|
||||
values[key] = decodeValue(val)
|
||||
elseif M.options.cumulative_parameters
|
||||
and type(values[key]) == 'string' then
|
||||
values[key] = { values[key] }
|
||||
table.insert(values[key], decodeValue(val))
|
||||
end
|
||||
|
||||
local t = values[key]
|
||||
for i,k in ipairs(keys) do
|
||||
if type(t) ~= 'table' then
|
||||
t = {}
|
||||
end
|
||||
if k == "" then
|
||||
k = #t+1
|
||||
end
|
||||
if not t[k] then
|
||||
t[k] = {}
|
||||
end
|
||||
if i == #keys then
|
||||
t[k] = val
|
||||
end
|
||||
t = t[k]
|
||||
end
|
||||
|
||||
end
|
||||
setmetatable(values, { __tostring = M.buildQuery })
|
||||
return values
|
||||
end
|
||||
|
||||
--- set the url query
|
||||
-- @param query Can be a string to parse or a table of key/value pairs
|
||||
-- @return a table representing the query key/value pairs
|
||||
function M:setQuery(query)
|
||||
local query = query
|
||||
if type(query) == 'table' then
|
||||
query = M.buildQuery(query)
|
||||
end
|
||||
self.query = M.parseQuery(query)
|
||||
return query
|
||||
end
|
||||
|
||||
--- set the authority part of the url
|
||||
-- The authority is parsed to find the user, password, port and host if available.
|
||||
-- @param authority The string representing the authority
|
||||
-- @return a string with what remains after the authority was parsed
|
||||
function M:setAuthority(authority)
|
||||
self.authority = authority
|
||||
self.port = nil
|
||||
self.host = nil
|
||||
self.userinfo = nil
|
||||
self.user = nil
|
||||
self.password = nil
|
||||
|
||||
authority = authority:gsub('^([^@]*)@', function(v)
|
||||
self.userinfo = v
|
||||
return ''
|
||||
end)
|
||||
|
||||
authority = authority:gsub(':(%d+)$', function(v)
|
||||
self.port = tonumber(v)
|
||||
return ''
|
||||
end)
|
||||
|
||||
local function getIP(str)
|
||||
-- ipv4
|
||||
local chunks = { str:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") }
|
||||
if #chunks == 4 then
|
||||
for _, v in pairs(chunks) do
|
||||
if tonumber(v) > 255 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
-- ipv6
|
||||
local chunks = { str:match("^%["..(("([a-fA-F0-9]*):"):rep(8):gsub(":$","%%]$"))) }
|
||||
if #chunks == 8 or #chunks < 8 and
|
||||
str:match('::') and not str:gsub("::", "", 1):match('::') then
|
||||
for _,v in pairs(chunks) do
|
||||
if #v > 0 and tonumber(v, 16) > 65535 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local ip = getIP(authority)
|
||||
if ip then
|
||||
self.host = ip
|
||||
elseif type(ip) == 'nil' then
|
||||
-- domain
|
||||
if authority ~= '' and not self.host then
|
||||
local host = authority:lower()
|
||||
if string.match(host, '^[%d%a%-%.]+$') ~= nil and
|
||||
string.sub(host, 0, 1) ~= '.' and
|
||||
string.sub(host, -1) ~= '.' and
|
||||
string.find(host, '%.%.') == nil then
|
||||
self.host = host
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.userinfo then
|
||||
local userinfo = self.userinfo
|
||||
userinfo = userinfo:gsub(':([^:]*)$', function(v)
|
||||
self.password = v
|
||||
return ''
|
||||
end)
|
||||
if string.find(userinfo, "^[%w%+%.]+$") then
|
||||
self.user = userinfo
|
||||
else
|
||||
-- incorrect userinfo
|
||||
self.userinfo = nil
|
||||
self.user = nil
|
||||
self.password = nil
|
||||
end
|
||||
end
|
||||
|
||||
return authority
|
||||
end
|
||||
|
||||
--- Parse the url into the designated parts.
|
||||
-- Depending on the url, the following parts can be available:
|
||||
-- scheme, userinfo, user, password, authority, host, port, path,
|
||||
-- query, fragment
|
||||
-- @param url Url string
|
||||
-- @return a table with the different parts and a few other functions
|
||||
function M.parse(url)
|
||||
local comp = {}
|
||||
M.setAuthority(comp, "")
|
||||
M.setQuery(comp, "")
|
||||
|
||||
local url = tostring(url or '')
|
||||
url = url:gsub('#(.*)$', function(v)
|
||||
comp.fragment = v
|
||||
return ''
|
||||
end)
|
||||
url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v)
|
||||
comp.scheme = v:lower()
|
||||
return ''
|
||||
end)
|
||||
url = url:gsub('%?(.*)', function(v)
|
||||
M.setQuery(comp, v)
|
||||
return ''
|
||||
end)
|
||||
url = url:gsub('^//([^/]*)', function(v)
|
||||
M.setAuthority(comp, v)
|
||||
return ''
|
||||
end)
|
||||
|
||||
comp.path = url:gsub("([^/]+)", function (s) return encode(decode(s), M.options.legal_in_path) end)
|
||||
|
||||
setmetatable(comp, {
|
||||
__index = M,
|
||||
__tostring = M.build,
|
||||
__concat = concat,
|
||||
__div = M.addSegment
|
||||
})
|
||||
return comp
|
||||
end
|
||||
|
||||
--- removes dots and slashes in urls when possible
|
||||
-- This function will also remove multiple slashes
|
||||
-- @param path The string representing the path to clean
|
||||
-- @return a string of the path without unnecessary dots and segments
|
||||
function M.removeDotSegments(path)
|
||||
local fields = {}
|
||||
if string.len(path) == 0 then
|
||||
return ""
|
||||
end
|
||||
local startslash = false
|
||||
local endslash = false
|
||||
if string.sub(path, 1, 1) == "/" then
|
||||
startslash = true
|
||||
end
|
||||
if (string.len(path) > 1 or startslash == false) and string.sub(path, -1) == "/" then
|
||||
endslash = true
|
||||
end
|
||||
|
||||
path:gsub('[^/]+', function(c) table.insert(fields, c) end)
|
||||
|
||||
local new = {}
|
||||
local j = 0
|
||||
|
||||
for i,c in ipairs(fields) do
|
||||
if c == '..' then
|
||||
if j > 0 then
|
||||
j = j - 1
|
||||
end
|
||||
elseif c ~= "." then
|
||||
j = j + 1
|
||||
new[j] = c
|
||||
end
|
||||
end
|
||||
local ret = ""
|
||||
if #new > 0 and j > 0 then
|
||||
ret = table.concat(new, '/', 1, j)
|
||||
else
|
||||
ret = ""
|
||||
end
|
||||
if startslash then
|
||||
ret = '/'..ret
|
||||
end
|
||||
if endslash then
|
||||
ret = ret..'/'
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function reducePath(base_path, relative_path)
|
||||
if string.sub(relative_path, 1, 1) == "/" then
|
||||
return '/' .. string.gsub(relative_path, '^[%./]+', '')
|
||||
end
|
||||
local path = base_path
|
||||
local startslash = string.sub(path, 1, 1) ~= "/";
|
||||
if relative_path ~= "" then
|
||||
path = (startslash and '' or '/') .. path:gsub("[^/]*$", "")
|
||||
end
|
||||
path = path .. relative_path
|
||||
path = path:gsub("([^/]*%./)", function (s)
|
||||
if s ~= "./" then return s else return "" end
|
||||
end)
|
||||
path = string.gsub(path, "/%.$", "/")
|
||||
local reduced
|
||||
while reduced ~= path do
|
||||
reduced = path
|
||||
path = string.gsub(reduced, "([^/]*/%.%./)", function (s)
|
||||
if s ~= "../../" then return "" else return s end
|
||||
end)
|
||||
end
|
||||
path = string.gsub(path, "([^/]*/%.%.?)$", function (s)
|
||||
if s ~= "../.." then return "" else return s end
|
||||
end)
|
||||
local reduced
|
||||
while reduced ~= path do
|
||||
reduced = path
|
||||
path = string.gsub(reduced, '^/?%.%./', '')
|
||||
end
|
||||
return (startslash and '' or '/') .. path
|
||||
end
|
||||
|
||||
--- builds a new url by using the one given as parameter and resolving paths
|
||||
-- @param other A string or a table representing a url
|
||||
-- @return a new url table
|
||||
function M:resolve(other)
|
||||
if type(self) == "string" then
|
||||
self = M.parse(self)
|
||||
end
|
||||
if type(other) == "string" then
|
||||
other = M.parse(other)
|
||||
end
|
||||
if other.scheme then
|
||||
return other
|
||||
else
|
||||
other.scheme = self.scheme
|
||||
if not other.authority or other.authority == "" then
|
||||
other:setAuthority(self.authority)
|
||||
if not other.path or other.path == "" then
|
||||
other.path = self.path
|
||||
local query = other.query
|
||||
if not query or not next(query) then
|
||||
other.query = self.query
|
||||
end
|
||||
else
|
||||
other.path = reducePath(self.path, other.path)
|
||||
end
|
||||
end
|
||||
return other
|
||||
end
|
||||
end
|
||||
|
||||
--- normalize a url path following some common normalization rules
|
||||
-- described on <a href="http://en.wikipedia.org/wiki/URL_normalization">The URL normalization page of Wikipedia</a>
|
||||
-- @return the normalized path
|
||||
function M:normalize()
|
||||
if type(self) == 'string' then
|
||||
self = M.parse(self)
|
||||
end
|
||||
if self.path then
|
||||
local path = self.path
|
||||
path = reducePath(path, "")
|
||||
-- normalize multiple slashes
|
||||
path = string.gsub(path, "//+", "/")
|
||||
self.path = path
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return M
|
|
@ -13,6 +13,7 @@ M.HTTP_CODE["401"] = ngx.HTTP_UNAUTHORIZED
|
|||
M.HTTP_CODE["403"] = ngx.HTTP_FORBIDDEN
|
||||
M.HTTP_CODE["404"] = ngx.HTTP_NOT_FOUND
|
||||
M.HTTP_CODE["405"] = ngx.HTTP_NOT_ALLOWED
|
||||
M.HTTP_CODE["406"] = ngx.HTTP_NOT_ACCEPTABLE
|
||||
M.HTTP_CODE["500"] = ngx.HTTP_INTERNAL_SERVER_ERROR
|
||||
|
||||
function M.read_file(path)
|
||||
|
|
Loading…
Reference in a new issue