mirror of
https://gitgud.io/fatchan/haproxy-protection.git
synced 2025-05-09 02:05:37 +00:00
refactor: remove ratelimiting functionality,
add on-demand global / per-domain ddos protection enabling add automatic redirect from captcha page back to the requested source prettify the captcha page
This commit is contained in:
27
src/cli/ddos_cli.sh
Executable file
27
src/cli/ddos_cli.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
_help() {
|
||||
/bin/cat <<EOF
|
||||
Usage: $0 <command> [options]
|
||||
|
||||
Show help screen and exit.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Commands:
|
||||
Global management:
|
||||
$0 global status Show status of global server ddos mode.
|
||||
$0 global enable Enable global ddos mode.
|
||||
$0 global disable Disable global ddos mode.
|
||||
|
||||
Domain management:
|
||||
$0 domain list List all domains with ddos mode on.
|
||||
$0 domain status <domain> Get ddos mode status for a domain.
|
||||
$0 domain add <domain> Enable ddos mode for a domain.
|
||||
$0 domain del <domain> Disable ddos mode for a domain.
|
||||
EOF
|
||||
}
|
||||
if ! [[ ${@} ]]; then
|
||||
_help
|
||||
fi
|
1559
src/libs/JSON.lua
Normal file
1559
src/libs/JSON.lua
Normal file
File diff suppressed because it is too large
Load Diff
82
src/libs/cookie.lua
Normal file
82
src/libs/cookie.lua
Normal file
@ -0,0 +1,82 @@
|
||||
-- Copyright (C) 2013-2016 Jiale Zhi (calio), CloudFlare Inc.
|
||||
-- See RFC6265 http://tools.ietf.org/search/rfc6265
|
||||
-- require "luacov"
|
||||
|
||||
local type = type
|
||||
local byte = string.byte
|
||||
local sub = string.sub
|
||||
|
||||
local EQUAL = byte("=")
|
||||
local SEMICOLON = byte(";")
|
||||
local SPACE = byte(" ")
|
||||
local HTAB = byte("\t")
|
||||
|
||||
|
||||
local _M = {}
|
||||
_M._VERSION = '0.01'
|
||||
|
||||
|
||||
function _M.get_cookie_table(text_cookie)
|
||||
if type(text_cookie) ~= "string" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local EXPECT_KEY = 1
|
||||
local EXPECT_VALUE = 2
|
||||
local EXPECT_SP = 3
|
||||
|
||||
local n = 0
|
||||
local len = #text_cookie
|
||||
|
||||
for i=1, len do
|
||||
if byte(text_cookie, i) == SEMICOLON then
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
|
||||
local cookie_table = {}
|
||||
|
||||
local state = EXPECT_SP
|
||||
local i = 1
|
||||
local j = 1
|
||||
local key, value
|
||||
|
||||
while j <= len do
|
||||
if state == EXPECT_KEY then
|
||||
if byte(text_cookie, j) == EQUAL then
|
||||
key = sub(text_cookie, i, j - 1)
|
||||
state = EXPECT_VALUE
|
||||
i = j + 1
|
||||
end
|
||||
elseif state == EXPECT_VALUE then
|
||||
if byte(text_cookie, j) == SEMICOLON
|
||||
or byte(text_cookie, j) == SPACE
|
||||
or byte(text_cookie, j) == HTAB
|
||||
then
|
||||
value = sub(text_cookie, i, j - 1)
|
||||
cookie_table[key] = value
|
||||
|
||||
key, value = nil, nil
|
||||
state = EXPECT_SP
|
||||
i = j + 1
|
||||
end
|
||||
elseif state == EXPECT_SP then
|
||||
if byte(text_cookie, j) ~= SPACE
|
||||
and byte(text_cookie, j) ~= HTAB
|
||||
then
|
||||
state = EXPECT_KEY
|
||||
i = j
|
||||
j = j - 1
|
||||
end
|
||||
end
|
||||
j = j + 1
|
||||
end
|
||||
|
||||
if key ~= nil and value == nil then
|
||||
cookie_table[key] = sub(text_cookie, i)
|
||||
end
|
||||
|
||||
return cookie_table
|
||||
end
|
||||
|
||||
return _M
|
96
src/libs/print_r.lua
Normal file
96
src/libs/print_r.lua
Normal file
@ -0,0 +1,96 @@
|
||||
-- Copyright 2016 Thierry Fournier
|
||||
|
||||
function color(index, str)
|
||||
return "\x1b[" .. index .. "m" .. str .. "\x1b[00m"
|
||||
end
|
||||
|
||||
function nocolor(index, str)
|
||||
return str
|
||||
end
|
||||
|
||||
function sp(count)
|
||||
local spaces = ""
|
||||
while count > 0 do
|
||||
spaces = spaces .. " "
|
||||
count = count - 1
|
||||
end
|
||||
return spaces
|
||||
end
|
||||
|
||||
function escape(str)
|
||||
local s = ""
|
||||
for i = 1, #str do
|
||||
local c = str:sub(i,i)
|
||||
ascii = string.byte(c, 1)
|
||||
if ascii > 126 or ascii < 20 then
|
||||
s = s .. string.format("\\x%02x", ascii)
|
||||
else
|
||||
s = s .. c
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
function print_rr(p, indent, c, wr, hist)
|
||||
local i = 0
|
||||
local nl = ""
|
||||
|
||||
if type(p) == "table" then
|
||||
wr(c("33", "(table)") .. " " .. c("36", tostring(p)) .. " [")
|
||||
|
||||
for idx, value in ipairs(hist) do
|
||||
if value == p then
|
||||
wr(" " .. c("35", "/* recursion */") .. " ]")
|
||||
return
|
||||
end
|
||||
end
|
||||
hist[indent + 1] = p
|
||||
|
||||
mt = getmetatable(p)
|
||||
if mt ~= nil then
|
||||
wr("\n" .. sp(indent+1) .. c("31", "METATABLE") .. ": ")
|
||||
print_rr(mt, indent+1, c, wr, hist)
|
||||
end
|
||||
|
||||
for k,v in pairs(p) do
|
||||
if i > 0 then
|
||||
nl = "\n"
|
||||
else
|
||||
wr("\n")
|
||||
end
|
||||
wr(nl .. sp(indent+1))
|
||||
if type(k) == "number" then
|
||||
wr(c("32", tostring(k)))
|
||||
else
|
||||
wr("\"" .. c("32", escape(tostring(k))) .. "\"")
|
||||
end
|
||||
wr(": ")
|
||||
print_rr(v, indent+1, c, wr, hist)
|
||||
i = i + 1
|
||||
end
|
||||
if i == 0 then
|
||||
wr(" " .. c("35", "/* empty */") .. " ]")
|
||||
else
|
||||
wr("\n" .. sp(indent) .. "]")
|
||||
end
|
||||
|
||||
hist[indent + 1] = nil
|
||||
|
||||
elseif type(p) == "string" then
|
||||
wr(c("33", "(string)") .. " \"" .. c("36", escape(p)) .. "\"")
|
||||
else
|
||||
wr(c("33", "(" .. type(p) .. ")") .. " " .. c("36", tostring(p)))
|
||||
end
|
||||
end
|
||||
|
||||
function print_r(p, col, wr)
|
||||
if col == nil then col = true end
|
||||
if wr == nil then wr = function(msg) io.stdout:write(msg) end end
|
||||
local hist = {}
|
||||
if col == true then
|
||||
print_rr(p, 0, color, wr, hist)
|
||||
else
|
||||
print_rr(p, 0, nocolor, wr, hist)
|
||||
end
|
||||
wr("\n")
|
||||
end
|
18
src/libs/utils.lua
Normal file
18
src/libs/utils.lua
Normal file
@ -0,0 +1,18 @@
|
||||
local _M = {}
|
||||
local md5 = require("md5")
|
||||
|
||||
function _M.get_hostname()
|
||||
local f = io.popen ("/bin/hostname")
|
||||
local hostname = f:read("*a") or ""
|
||||
f:close()
|
||||
hostname =string.gsub(hostname, "\n$", "")
|
||||
return hostname
|
||||
end
|
||||
|
||||
function _M.get_floating_hash()
|
||||
-- This ensures that a cookie is rotated every day
|
||||
return md5.sumhexa(_M.get_hostname() .. os.date("%d"))
|
||||
end
|
||||
|
||||
return _M
|
||||
|
97
src/scripts/hcaptcha.lua
Normal file
97
src/scripts/hcaptcha.lua
Normal file
@ -0,0 +1,97 @@
|
||||
_M = {}
|
||||
|
||||
local url = require("net.url")
|
||||
local https = require("ssl.https")
|
||||
local json = require("json")
|
||||
local utils = require("utils")
|
||||
local cookie = require("cookie")
|
||||
|
||||
local floating_hash = utils.get_floating_hash()
|
||||
local hcaptcha_secret = os.getenv("HCAPTCHA_SECRET")
|
||||
local hcaptcha_sitekey = os.getenv("HCAPTCHA_SITEKEY")
|
||||
|
||||
function _M.view(applet)
|
||||
local response_body
|
||||
local response_status_code
|
||||
|
||||
if applet.method == "GET" then
|
||||
response_body =
|
||||
[[
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Captcha</title>
|
||||
<style>
|
||||
body {
|
||||
width: 35em;
|
||||
margin: 0 auto;
|
||||
font-family: Tahoma, Verdana, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Captcha challenge completion required.</h1>
|
||||
<p>We have detected an unusual activity on the requested resource.</p>
|
||||
<p>To ensure that the service runs smoothly, it is needed to complete a captcha challenge.</p>
|
||||
<form method="POST">
|
||||
<div class="h-captcha" data-sitekey="%s"></div>
|
||||
<script src="https://hcaptcha.com/1/api.js" async defer></script>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
<p><em>Thank you for understanding.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
]]
|
||||
response_body = string.format(response_body, hcaptcha_sitekey)
|
||||
response_status_code = 200
|
||||
elseif applet.method == "POST" then
|
||||
local parsed_body = url.parseQuery(applet.receive(applet))
|
||||
|
||||
if parsed_body["h-captcha-response"] then
|
||||
local url =
|
||||
string.format(
|
||||
"https://hcaptcha.com/siteverify?secret=%s&response=%s",
|
||||
hcaptcha_secret,
|
||||
parsed_body["h-captcha-response"]
|
||||
)
|
||||
local body, _, _, _ = https.request(url)
|
||||
local api_response = json:decode(body)
|
||||
|
||||
if api_response.success == true then
|
||||
core.Debug("HCAPTCHA SUCCESSFULLY PASSED")
|
||||
applet:add_header(
|
||||
"set-cookie",
|
||||
string.format("z_ddos_protection=%s; Max-Age=14400; Path=/", floating_hash)
|
||||
)
|
||||
else
|
||||
core.Debug("HCAPTCHA FAILED: " .. body)
|
||||
end
|
||||
end
|
||||
|
||||
response_body = "Thank you for submitting!"
|
||||
response_status_code = 301
|
||||
applet:add_header("location", applet.qs)
|
||||
end
|
||||
|
||||
applet:set_status(response_status_code)
|
||||
applet:add_header("content-type", "text/html")
|
||||
applet:add_header("content-length", string.len(response_body))
|
||||
applet:start_response()
|
||||
applet:send(response_body)
|
||||
end
|
||||
|
||||
function _M.check_captcha_status(txn)
|
||||
core.Debug("CAPTCHA STATUS CHECK START")
|
||||
txn:set_var("txn.requested_url", "/mopsik?kek=pek")
|
||||
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
|
||||
|
||||
core.Debug("RECEIVED SECRET COOKIE: " .. parsed_request_cookies["z_ddos_protection"])
|
||||
core.Debug("OUR SECRET COOKIE: " .. floating_hash)
|
||||
|
||||
if parsed_request_cookies["z_ddos_protection"] == floating_hash then
|
||||
core.Debug("CAPTCHA STATUS CHECK SUCCESS")
|
||||
return txn:set_var("txn.captcha_passed", true)
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
6
src/scripts/register.lua
Normal file
6
src/scripts/register.lua
Normal file
@ -0,0 +1,6 @@
|
||||
package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/scripts/?.lua;/usr/local/etc/haproxy/libs/?.lua"
|
||||
|
||||
local hcaptcha = require("hcaptcha")
|
||||
|
||||
core.register_service("hcaptcha-view", "http", hcaptcha.view)
|
||||
core.register_action("hcaptcha-redirect", { 'http-req', }, hcaptcha.check_captcha_status)
|
Reference in New Issue
Block a user