EndGame/lua/cap.lua

283 lines
9.2 KiB
Lua

-- encryption key and salt must be shared across fronts. salt must be 8 chars
local key = "encryption_key"
local salt = "salt1234"
-- for how long the captcha is valid. 120 sec is for testing, 3600 1 hour should be production.
local session_timeout = 3600
aes = require "resty.aes"
str = require "resty.string"
cook = require "resty.cookie"
aes_128_cbc_sha512x1 = aes:new(key, salt, aes.cipher(128,"cbc"), aes.hash.sha512, 1)
local cookie, err = cook:new()
if not cookie then
ngx.log(ngx.ERR, err)
return
end
function fromhex(str)
return (str:gsub('..', function (cc)
return string.char(tonumber(cc, 16))
end))
end
function tohex(str)
return (str:gsub('.', function (c)
return string.format('%02X', string.byte(c))
end))
end
caperror = nil
-- check proxy_protocol_addr if present kill circuit if needed
local pa = "no_proxy"
if ngx.var.proxy_protocol_addr ~= nil then
pa = ngx.var.proxy_protocol_addr
end
-- if "Host" header is invalid / missing kill circuit and return nothing
if in_array(allowed_hosts, ngx.var.http_host) == nil then
ngx.log(ngx.ERR, "Wrong host (" .. ngx.var.http_host .. ") " .. ngx.var.remote_addr .. "|" .. pa)
if pa ~= "no_proxy" then
local ok, err = ngx.timer.at(0, kill_circuit, ngx.var.remote_addr, ngx.var.proxy_protocol_addr)
if not ok then
ngx.log(ngx.ERR, "failed to create timer: ", err)
return
end
end
ngx.exit(444)
end
-- only GET and POST requests are allowed the others are not used. HEAD for recon checker
if ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "GET" and ngx.var.request_method ~= "HEAD" then
ngx.log(ngx.ERR, "Wrong request (" .. ngx.var.request_method .. ") " .. ngx.var.remote_addr .. "|" .. pa)
if pa ~= "no_proxy" then
local ok, err = ngx.timer.at(0, kill_circuit, ngx.var.remote_addr, ngx.var.proxy_protocol_addr)
if not ok then
ngx.log(ngx.ERR, "failed to create timer: ", err)
return
end
end
ngx.exit(444)
end
-- requests without user-agent are usually invalid
if ngx.var.http_user_agent == nil then
ngx.log(ngx.ERR, "Missing user agent " .. ngx.var.remote_addr .. "|" .. pa)
if pa ~= "no_proxy" then
local ok, err = ngx.timer.at(0, kill_circuit, ngx.var.remote_addr, ngx.var.proxy_protocol_addr)
if not ok then
ngx.log(ngx.ERR, "failed to create timer: ", err)
return
end
end
ngx.exit(444)
end
-- POST without referer is invalid. some poorly configured clients may complain about this
if ngx.var.request_method == "POST" and ngx.var.http_referer == nil then
ngx.log(ngx.ERR, "Post without referer " .. ngx.var.remote_addr .. "|" .. pa)
if pa ~= "no_proxy" then
local ok, err = ngx.timer.at(0, kill_circuit, ngx.var.remote_addr, ngx.var.proxy_protocol_addr)
if not ok then
ngx.log(ngx.ERR, "failed to create timer: ", err)
return
end
end
ngx.exit(444)
end
-- check cookie support similar to testcookie
if ngx.var.request_method == "GET" then
local args = ngx.req.get_uri_args()
if args['tca'] == "1" then
local field, err = cookie:get("dcap")
if err or not field then
ngx.exit(403)
end
-- if cookie cannot be decrypted most likely it has been messed with
local cookdata = aes_128_cbc_sha512x1:decrypt(fromhex(field))
if not cookdata then
ngx.header.content_type = 'text/plain'
ngx.say("403 DDOS fliter killed your path. (You probably sent too many requests at once). Not calling you a bot, bot, but grab a new identity and try again.")
ngx.flush()
ngx.exit(200)
end
cooktest = split(cookdata, "|")[1]
if cooktest ~= "cap_not_solved" and cooktest ~= "captcha_solved" then
ngx.exit(403)
end
end
-- try to set cookie. max-age is irrelevant as it can be faked and check is done against cookie content anyway. should be set to a large value otherwise it will annoy users
local field, err = cookie:get("dcap")
if err then
local tstamp = ngx.now()
local plaintext = "cap_not_solved|" .. tstamp .. "|1"
local ciphertext = tohex(aes_128_cbc_sha512x1:encrypt(plaintext))
local ok, err = cookie:set({
key = "dcap", value = ciphertext, path = "/",
domain = ngx.var.host, httponly = true,
max_age = 21600,
samesite = "Strict"
})
if not ok then
ngx.log(ngx.ERR, err)
return
end
ngx.header.content_type = 'text/html'
ngx.say("<head> \
<meta http-equiv=\"refresh\" content=\"1\"> \
</head><a href=\"/\">One moment...</p>")
ngx.flush()
ngx.exit(200)
end
end
-- captcha generator functions
require "caphtml_d"
local field, err = cookie:get("dcap")
if not field or field == nil then
displaycap()
ngx.flush()
ngx.exit(200)
end
-- check if cookie is blacklisted by rate limiter. if it is show the client a message and exit. can get creative with this.
local blocked_cookies = ngx.shared.blocked_cookies
local bct, btcflags = blocked_cookies:get(field)
if bct then
ngx.log(ngx.ERR, "Cookie " .. field .. " blacklisted.")
ngx.header.content_type = 'text/plain'
ngx.say("403 DDOS fliter killed your path. (You probably sent too many requests at once). Not calling you a bot, bot, but grab a new identity and try again.")
ngx.flush()
ngx.exit(200)
end
if ngx.var.request_method == "POST" then
local field, err = cookie:get("dcap")
if err then
ngx.exit(403)
end
if field then
plaintext = aes_128_cbc_sha512x1:decrypt(fromhex(field))
if not plaintext then
ngx.header.content_type = 'text/plain'
ngx.say("403 DDOS fliter killed your path. (You probably sent too many requests at once). Not calling you a bot, bot, but grab a new identity and try again.")
ngx.flush()
ngx.exit(200)
end
cookdata = split(plaintext,"|")
local expired = nil
if (tonumber(cookdata[2]) + session_timeout) < ngx.now() then
expired = true
caperror = "Session expired"
displaycap()
ngx.flush()
ngx.exit(200)
end
if cookdata[1] == "captcha_solved" and not expired then
return
end
end
-- resty has a library for parsing POST data but it's not really needed
ngx.req.read_body()
local dataraw = ngx.req.get_body_data()
if dataraw == nil then
caperror = "You didn't submit anything. Try again."
displaycap()
ngx.flush()
ngx.exit(200)
end
local data = ngx.req.get_body_data()
data = split(data, "&")
local sentcap = ""
for index, value in ipairs(data) do
sentcap = sentcap .. split(value,"=")[2]
end
if field then
plaintext = aes_128_cbc_sha512x1:decrypt(fromhex(field))
if not plaintext then
ngx.header.content_type = 'text/plain'
ngx.say("403 DDOS fliter killed your path. (You probably sent too many requests at once). Not calling you a bot, bot, but grab a new identity and try again.")
ngx.flush()
ngx.exit(200)
end
cookdata = split(plaintext,"|")
if (tonumber(cookdata[2]) + 60) < ngx.now() then
caperror = "Captcha expired"
displaycap()
ngx.flush()
ngx.exit(200)
end
if sentcap == cookdata[3] then
local newcookdata = ""
cookdata[1] = "captcha_solved"
for k,v in pairs(cookdata) do
newcookdata = newcookdata .. "|" .. v
end
local tstamp = ngx.now()
local ciphertext = tohex(aes_128_cbc_sha512x1:encrypt(newcookdata))
local ok, err = cookie:set({
key = "dcap", value = ciphertext, path = "/",
domain = ngx.var.host, httponly = true,
max_age = 21600,
samesite = "Strict"
})
if not ok then
ngx.say("cookie error")
return
end
local redirect_to = ngx.var.uri
if ngx.var.query_string ~= nil then
redirect_to = redirect_to .. "?" .. ngx.var.query_string
end
return ngx.redirect(redirect_to)
else
caperror = "You Got That Wrong. Try again"
end
else
caperror = "Session invalid or expired"
displaycap()
ngx.flush()
ngx.exit(200)
end
end
plaintext = aes_128_cbc_sha512x1:decrypt(fromhex(field))
if not plaintext then
ngx.header.content_type = 'text/plain'
ngx.say("403 DDOS fliter killed your path. (You probably sent too many requests at once). Not calling you a bot, bot, but grab a new identity and try again.")
ngx.flush()
ngx.exit(200)
end
cookdata = split(plaintext,"|")
if not cookdata then
displaycap()
ngx.flush()
ngx.exit(200)
end
local expired = nil
if (tonumber(cookdata[2]) + session_timeout) < ngx.now() then
expired = true
caperror = "Session expired"
end
if cookdata[1] ~= "captcha_solved" or expired then
displaycap()
ngx.flush()
ngx.exit(200)
end