Sindbad~EG File Manager

Current Path : /opt/nginxhttpd_/etc/openresty_config/lua/lib/
Upload File :
Current File : //opt/nginxhttpd_/etc/openresty_config/lua/lib/o2switch_captcha.lua

--[[
    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