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:
@@ -8,7 +8,9 @@ services:
|
|||||||
- 80:80
|
- 80:80
|
||||||
volumes:
|
volumes:
|
||||||
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
|
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
|
||||||
- ./scripts/:/usr/local/etc/haproxy/scripts/
|
- ./haproxy/domains_under_ddos.txt:/usr/local/etc/haproxy/domains_under_ddos.txt
|
||||||
|
- ./src/scripts/:/usr/local/etc/haproxy/scripts/
|
||||||
|
- ./src/libs/:/usr/local/etc/haproxy/libs/
|
||||||
environment:
|
environment:
|
||||||
- HCAPTCHA_SECRET=${HCAPTCHA_SECRET}
|
- HCAPTCHA_SECRET=${HCAPTCHA_SECRET}
|
||||||
- HCAPTCHA_SITEKEY=${HCAPTCHA_SITEKEY}
|
- HCAPTCHA_SITEKEY=${HCAPTCHA_SITEKEY}
|
||||||
|
@@ -97,7 +97,7 @@ ADD haproxy/docker-entrypoint.sh /usr/local/bin/
|
|||||||
RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat
|
RUN ln -s usr/local/bin/docker-entrypoint.sh / # backwards compat
|
||||||
|
|
||||||
# This is terrible mess but we need it for simple testing purposes of our POC
|
# This is terrible mess but we need it for simple testing purposes of our POC
|
||||||
RUN apt-get update && apt-get install libssl-dev make nano wget gcc libreadline-dev unzip git -y
|
RUN apt-get update && apt-get install libssl-dev make nano wget gcc libreadline-dev unzip git socat -y
|
||||||
RUN wget http://www.lua.org/ftp/lua-5.3.5.tar.gz &&\
|
RUN wget http://www.lua.org/ftp/lua-5.3.5.tar.gz &&\
|
||||||
tar -zxf lua-5.3.5.tar.gz &&\
|
tar -zxf lua-5.3.5.tar.gz &&\
|
||||||
cd lua-5.3.5 &&\
|
cd lua-5.3.5 &&\
|
||||||
|
0
haproxy/domains_under_ddos.txt
Normal file
0
haproxy/domains_under_ddos.txt
Normal file
@@ -3,6 +3,7 @@ global
|
|||||||
maxconn 256
|
maxconn 256
|
||||||
log stdout format raw local0 debug
|
log stdout format raw local0 debug
|
||||||
lua-load /usr/local/etc/haproxy/scripts/register.lua
|
lua-load /usr/local/etc/haproxy/scripts/register.lua
|
||||||
|
stats socket /var/run/haproxy.sock mode 666 level admin
|
||||||
|
|
||||||
defaults
|
defaults
|
||||||
mode http
|
mode http
|
||||||
@@ -12,16 +13,17 @@ defaults
|
|||||||
|
|
||||||
frontend http-in
|
frontend http-in
|
||||||
bind *:80
|
bind *:80
|
||||||
default_backend servers
|
|
||||||
|
|
||||||
http-request lua.hcaptcha-redirect if !{ path -m beg /captcha }
|
|
||||||
http-request use-service lua.hello-world if { path /hello_world }
|
|
||||||
http-request use-service lua.hcaptcha-view if { path /captcha/ }
|
|
||||||
http-request lua.ratelimit if !{ path -m beg /captcha }
|
|
||||||
|
|
||||||
|
acl ddos_mode_enabled hdr_cnt(xr3la1rfFc) eq 0
|
||||||
|
acl domain_under_ddos hdr(host) -i -f /usr/local/etc/haproxy/domains_under_ddos.txt
|
||||||
acl captcha_passed var(txn.captcha_passed) -m bool
|
acl captcha_passed var(txn.captcha_passed) -m bool
|
||||||
acl on_captcha_url path -m beg /captcha
|
acl on_captcha_url path -m beg /captcha
|
||||||
redirect prefix /captcha code 301 if !captcha_passed !on_captcha_url
|
|
||||||
|
http-request lua.hcaptcha-redirect if !{ path -m beg /captcha }
|
||||||
|
http-request use-service lua.hcaptcha-view if { path /captcha }
|
||||||
|
http-request redirect location /captcha?%[capture.req.uri] code 301 if !captcha_passed !on_captcha_url ddos_mode_enabled OR domain_under_ddos
|
||||||
|
|
||||||
|
default_backend servers
|
||||||
|
|
||||||
backend servers
|
backend servers
|
||||||
server server1 nginx:80 maxconn 32
|
server server1 nginx:80 maxconn 32
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
|
|
@@ -1,10 +0,0 @@
|
|||||||
guard = {}
|
|
||||||
|
|
||||||
function guard.hello_world(applet)
|
|
||||||
applet:set_status(200)
|
|
||||||
local response = string.format([[<html><body>Hello World!</body></html>]], message);
|
|
||||||
applet:add_header("content-type", "text/html");
|
|
||||||
applet:add_header("content-length", string.len(response))
|
|
||||||
applet:start_response()
|
|
||||||
applet:send(response)
|
|
||||||
end
|
|
@@ -1,81 +0,0 @@
|
|||||||
hcaptcha = {}
|
|
||||||
|
|
||||||
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 maximun_requests_per_expire = 5
|
|
||||||
|
|
||||||
function hcaptcha.view(applet)
|
|
||||||
local hcaptcha_secret = os.getenv("HCAPTCHA_SECRET")
|
|
||||||
local hcaptcha_sitekey = os.getenv("HCAPTCHA_SITEKEY")
|
|
||||||
local response
|
|
||||||
if applet.method == "GET" then
|
|
||||||
response =
|
|
||||||
[[
|
|
||||||
<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>
|
|
||||||
]]
|
|
||||||
response = string.format(response, hcaptcha_sitekey)
|
|
||||||
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, code, headers, status = https.request(url)
|
|
||||||
local api_response = json:decode(body)
|
|
||||||
|
|
||||||
if api_response.success == true then
|
|
||||||
print("HCAPTCHA SUCCESSFULLY PASSED")
|
|
||||||
applet:add_header("set-cookie", string.format("z_ddos_protection=%s; Max-Age=14400; Path=/", floating_hash))
|
|
||||||
else
|
|
||||||
print("HCAPTCHA FAILED", body)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
response = "Thank you for submitting"
|
|
||||||
end
|
|
||||||
|
|
||||||
applet:set_status(200)
|
|
||||||
applet:add_header("content-type", "text/html")
|
|
||||||
applet:add_header("content-length", string.len(response))
|
|
||||||
applet:start_response()
|
|
||||||
applet:send(response)
|
|
||||||
end
|
|
||||||
|
|
||||||
function hcaptcha.check_captcha_status(txn)
|
|
||||||
local host = txn.sf:hdr("Host")
|
|
||||||
local current_requests_count = client:llen(host)
|
|
||||||
|
|
||||||
print("CURRENT REQUESTS COUNT: ", current_requests_count)
|
|
||||||
print("MAXIMUM REQUESTS COUNT: ", maximun_requests_per_expire)
|
|
||||||
|
|
||||||
if current_requests_count > maximun_requests_per_expire then
|
|
||||||
print("CAPTCHA STATUS CHECK START")
|
|
||||||
local raw_request_cookies = txn.sf:hdr("Cookie")
|
|
||||||
local parsed_request_cookies = cookie.get_cookie_table(raw_request_cookies)
|
|
||||||
|
|
||||||
print("RECEIVED SECRET COOKIE: ", parsed_request_cookies["z_ddos_protection"])
|
|
||||||
print("OUR SECRET COOKIE: ", floating_hash)
|
|
||||||
|
|
||||||
if parsed_request_cookies["z_ddos_protection"] == floating_hash then
|
|
||||||
print("CAPTCHA STATUS CHECK SUCCESS")
|
|
||||||
return txn:set_var("txn.captcha_passed", true);
|
|
||||||
end
|
|
||||||
|
|
||||||
print("CAPTCHA STATUS CHECK FINISH")
|
|
||||||
else
|
|
||||||
return txn:set_var("txn.captcha_passed", true);
|
|
||||||
end
|
|
||||||
end
|
|
1203
scripts/redis.lua
1203
scripts/redis.lua
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/scripts/?.lua"
|
|
||||||
test = {}
|
|
||||||
local redis = require 'redis'
|
|
||||||
client = redis.connect('redis', 6379)
|
|
||||||
local expire_time = 120
|
|
||||||
|
|
||||||
function test.ratelimit(txn)
|
|
||||||
local host = txn.sf:hdr("Host")
|
|
||||||
client:rpush(host,host)
|
|
||||||
client:expire(host, expire_time)
|
|
||||||
end
|
|
@@ -1,8 +0,0 @@
|
|||||||
package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/scripts/?.lua"
|
|
||||||
require("redis")
|
|
||||||
|
|
||||||
local redis = require 'redis'
|
|
||||||
local client = redis.connect('127.0.0.1', 6379)
|
|
||||||
local response = client:ping()
|
|
||||||
local dummy = client:get('dummy')
|
|
||||||
print(dummy)
|
|
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
|
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
|
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
|
@@ -1,11 +1,6 @@
|
|||||||
package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/scripts/?.lua"
|
package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/scripts/?.lua;/usr/local/etc/haproxy/libs/?.lua"
|
||||||
|
|
||||||
require("guard")
|
local hcaptcha = require("hcaptcha")
|
||||||
require("hcaptcha")
|
|
||||||
require("test")
|
|
||||||
|
|
||||||
|
|
||||||
core.register_service("hello-world", "http", guard.hello_world)
|
|
||||||
core.register_service("hcaptcha-view", "http", hcaptcha.view)
|
core.register_service("hcaptcha-view", "http", hcaptcha.view)
|
||||||
core.register_action("hcaptcha-redirect", { 'http-req', }, hcaptcha.check_captcha_status)
|
core.register_action("hcaptcha-redirect", { 'http-req', }, hcaptcha.check_captcha_status)
|
||||||
core.register_action("ratelimit", { 'http-req', }, test.ratelimit)
|
|
Reference in New Issue
Block a user