Sindbad~EG File Manager
--[[
This file contains the backend implementation for the Recaptcha / Hcaptcha / Friendly captcha service.
All the API quite similar so one implementation can validate almost all.
We can choose one or another with the config file.
--]]
local _M = {}
local o2config = require "lib/o2switch_config"
local o2debug = require "lib/o2switch_debug"
local httpClient = require "resty.http"
local json = require "cjson"
local salt = o2config.captchaSecret
local ngx = ngx
local md5 = ngx.md5
local today = ngx.today
local time = ngx.time
local cookieTime = ngx.cookie_time
local pcall = pcall
local type = type
local gsub = string.gsub
local string = string
local table = table
-- Trying to optimize lua strings. Not sure if it's working...
local strings = {
hcaptchaInclusion = '<script src="https://js.hcaptcha.com/1/api.js"></script>',
hcaptchaDiv = [=[
<div id="captchaBlock" style="margin-top:2em; margin-bottom:3em;">
<div id="hcaptcha" class="h-captcha" data-sitekey="]=].. o2config.captchaSiteKey .. [=[" data-callback="onHcaptchaSubmit" data-size="invisible"></div>
</div>
]=],
hcaptchaEndpoint = 'https://hcaptcha.com/siteverify',
recaptchaInclusion = '<script src="https://www.google.com/recaptcha/api.js" async></script>',
recaptchaDiv = [=[
<div id="captchaBlock" style="margin-top:2em; margin-bottom:3em;">
<div class="g-recaptcha" data-sitekey="]=].. o2config.captchaSiteKey .. [=[" data-callback="onRecaptchaSubmit"></div>
</div>
]=],
recaptchaEndpoint = 'https://www.google.com/recaptcha/api/siteverify',
friendlycaptchaInclusion = [=[
<script type="module" src="https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.8/widget.module.min.js" async defer></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.8/widget.min.js" async defer></script>
]=],
friendlycaptchaDiv = [=[
<div class="frc-captcha" data-sitekey="]=].. o2config.captchaSiteKey .. [=[" data-callback="onFriendlyCaptchaSubmit" data-start="auto"></div>
]=],
friendlycaptchaEndpoint = 'https://api.friendlycaptcha.com/api/v1/siteverify',
}
--o2debug.debug("Captcha method configured = " .. o2config.captchaProvider)
-- (private) Return the right javascript inclusion script tags based on the captcha provider selected
-- @return string
local function getJsScriptInclusion(captchaProvider)
if(captchaProvider == 'hcaptcha') then
return strings.hcaptchaInclusion
elseif (captchaProvider == 'recaptcha') then
return strings.recaptchaInclusion
elseif (captchaProvider == 'friendlycaptcha') then
return strings.friendlycaptchaInclusion
end
return ''
end
-- (private) Return the right captcha div/html based on the captcha provider selected
-- @return string
local function getCaptchaDiv(captchaProvider)
if(captchaProvider == 'hcaptcha') then
return strings.hcaptchaDiv
elseif (captchaProvider == 'recaptcha') then
return strings.recaptchaDiv
elseif (captchaProvider == 'friendlycaptcha') then
return strings.friendlycaptchaDiv
end
return ''
end
-- Optimization : Most of the template dont change.
-- So we can do a search/replace once for all for most of the values to replace
local function preFillTemplate(challengeTemplate)
return ((challengeTemplate:gsub('#(%w+)', {
['chlType'] = 'captcha',
['captchaKey'] = o2config.captchaSiteKey,
['captchaProvider'] = o2config.captchaProvider,
['captchaProviderJsInclusion'] = getJsScriptInclusion(o2config.captchaProvider),
['captchaDiv']= getCaptchaDiv(o2config.captchaProvider),
})))
end
local challengeTemplate = preFillTemplate(challengeTemplate)
-- Running a gsub on multiple pattern on the template is not efficient and king of slow
-- Since the template doesn't change for each requets, we can calculate the replacement position and use thoses
-- static position instead. We'll need to do the search/replace in reverse, starting by the last pattern found on the
-- challenge template.
local function getPositions()
local positions = {}
local item = {"#hashValue", "#requestId"}
for i=1, #item do
positions[i] = {
item[i],
string.find(challengeTemplate, item[i], 1)
}
end
table.sort(positions, function(a,b)
return a[2] > b[2]
end)
return positions
end
local positions = getPositions()
-- Fast search/replace of pattern using the position
-- @return string
local function fastReplace(template, start, stop, replacement)
return template:sub(1, start - 1)
.. replacement
.. template:sub(stop + 1, #template)
end
-- Fast search/replace to get the template
local function fastTemplate(replacementValues)
local response = challengeTemplate
for i =1, #positions do
response = fastReplace(
response,
positions[i][2],
positions[i][3],
replacementValues[positions[i][1]]
)
end
return response
end
-- (private) Return the verification URL for the captcha
-- @return string
local function getCaptchaVerificationUrl(captchaProvider)
if(captchaProvider == 'hcaptcha') then
return strings.hcaptchaEndpoint
elseif(captchaProvider == 'recaptcha') then
return strings.recaptchaEndpoint
elseif(captchaProvider == 'friendlycaptcha') then
return strings.friendlycaptchaEndpoint
end
return ''
end
-- Generate a hash with the client IP address, UA, challenge type and a secret, to avoid tampering of the challenge send.
-- For instance, to avoid a smartass to ask the JS challenge instead of captcha
-- @return md5 hash
function _M.getAntiTamperingHash(ip, ua)
return md5('ANTI-TAMPERING' .. ip .. (ua or '-') .. o2config.captchaProvider .. salt)
end
-- Send the HTML code for the Captcha Challenge
-- @antiTamperingHash the hash from getAntiTamperingHash
function _M.sendChallengeHtmlPage(antiTamperingHash, requestId)
ngx.status = 503
ngx.header.content_type = "text/html; charset=UTF-8"
ngx.header.cache_control = "private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0"
ngx.header.expires = "Thu, 01 Jan 1970 00:00:01 GMT"
ngx.header.referer_policy = "same-origin"
ngx.header.tiger_protect_security = "https://faq.o2switch.fr/hebergement-mutualise/tutoriels-cpanel/tiger-protect"
ngx.say(fastTemplate({
['#hashValue'] = antiTamperingHash,
['#requestId'] = requestId or '-',
}))
ngx.exit(200)
end
-- Validate the captcha response
-- @return bool is valid, errorMessage|nil
function _M.validate(response, remoteIp)
if not response or type(response) ~= 'string' then
--o2debug.debug("Response is not a string")
return false, "Response is not a string"
end
if not remoteIp or type(remoteIp) ~= 'string' then
--o2debug.debug("remoteIp is not a string")
return false, "remoteIp is not a string"
end
-- hcatpcha/recaptcha have the same API
local data = 'secret=' .. o2config.captchaSecretKey
.. '&response=' .. response
.. '&remoteip=' .. remoteIp
if (o2config.captchaProvider == 'friendlycaptcha') then
data = 'secret=' .. o2config.captchaSecretKey
.. '&sitekey=' .. o2config.captchaSiteKey
.. '&solution=' .. response
end
local httpc = httpClient.new()
httpc:set_timeout(2000)
local res, err = httpc:request_uri(
getCaptchaVerificationUrl(o2config.captchaProvider),
{
method = "POST",
body = data,
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
},
ssl_verify = false,
}
)
httpc:close()
if err ~= nil then
--o2debug.debug("HTTP error : " .. (err or 'no err msg'))
return true, err
end
local ok, result = pcall(json.decode, res.body)
if not ok then
--o2debug.debug("Error while trying to decode a json string")
return true, "Error while trying to decode a json string"
end
if result.success == false then
return true, "reCaptcha secret key is invalid"
end
return result.success, nil
end
-- Generate and return the value of the bypass cookie for the CAPTCHA challenge.
-- When this cookie is passed by the browser, the challenge is not triggered anymore.
-- @clientip
-- @ua
-- @domain
-- @return string
function _M.getBypassCookieValue(clientip, ua, domain)
return md5('CAPTCHA-CHL-BYPASS' .. o2config.captchaProvider .. clientip .. (ua or '-') .. domain .. salt .. today())
end
function _M.sendSuccessRedirectResponse(url, domain, cookieName, bypassCookieValue)
ngx.status = 302 -- TODO: try with 307 one day to see if we are able to keep the POST content
ngx.header.content_type = "text/html; charset=UTF-8"
ngx.header.cache_control = "private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0"
ngx.header.expires = "Thu, 01 Jan 1970 00:00:01 GMT"
ngx.header.referer_policy = "same-origin"
ngx.header.set_cookie = cookieName .. '=' .. bypassCookieValue .. '; domain=.'.. domain ..'; expires='.. cookieTime(time() + 86400) ..'; path=/; SameSite=Lax; HttpOnly'
ngx.header.tiger_protect_security = "https://faq.o2switch.fr/hebergement-mutualise/tutoriels-cpanel/tiger-protect"
ngx.header.location = url
ngx.say(((gsub([=[
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="#url">here</A>.
</BODY></HTML>
]=], '#url', url))))
ngx.exit(ngx.HTTP_OK)
end
return _M
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists