mirror of
https://gitgud.io/fatchan/haproxy-protection.git
synced 2025-05-09 02:05:37 +00:00
Reorganise, move code to not be split between haproxy and src folder
This commit is contained in:
89
src/lua/libs/cookie.lua
Normal file
89
src/lua/libs/cookie.lua
Normal file
@ -0,0 +1,89 @@
|
||||
-- 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 MAX_LEN = 10 * 1024 -- in case you are a dumbass and set a high tune.maxrewrite
|
||||
local MAX_COOKIES = 100
|
||||
|
||||
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
|
||||
if len > MAX_LEN then
|
||||
return {}
|
||||
end
|
||||
|
||||
for i=1, len do
|
||||
if byte(text_cookie, i) == SEMICOLON then
|
||||
n = n + 1
|
||||
if n > MAX_COOKIES then
|
||||
return {}
|
||||
end
|
||||
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
|
388
src/lua/libs/json.lua
Normal file
388
src/lua/libs/json.lua
Normal file
@ -0,0 +1,388 @@
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2020 rxi
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
-- this software and associated documentation files (the "Software"), to deal in
|
||||
-- the Software without restriction, including without limitation the rights to
|
||||
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
-- of the Software, and to permit persons to whom the Software is furnished to do
|
||||
-- so, subject to the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be included in all
|
||||
-- copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
--
|
||||
|
||||
local json = { _version = "0.1.2" }
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\",
|
||||
[ "\"" ] = "\"",
|
||||
[ "\b" ] = "b",
|
||||
[ "\f" ] = "f",
|
||||
[ "\n" ] = "n",
|
||||
[ "\r" ] = "r",
|
||||
[ "\t" ] = "t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if rawget(val, 1) ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
local line_count = 1
|
||||
local col_count = 1
|
||||
for i = 1, idx - 1 do
|
||||
col_count = col_count + 1
|
||||
if str:sub(i, i) == "\n" then
|
||||
line_count = line_count + 1
|
||||
col_count = 1
|
||||
end
|
||||
end
|
||||
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(1, 4), 16 )
|
||||
local n2 = tonumber( s:sub(7, 10), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local res = ""
|
||||
local j = i + 1
|
||||
local k = j
|
||||
|
||||
while j <= #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
|
||||
elseif x == 92 then -- `\`: Escape
|
||||
res = res .. str:sub(k, j - 1)
|
||||
j = j + 1
|
||||
local c = str:sub(j, j)
|
||||
if c == "u" then
|
||||
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
|
||||
or str:match("^%x%x%x%x", j + 1)
|
||||
or decode_error(str, j - 1, "invalid unicode escape in string")
|
||||
res = res .. parse_unicode_escape(hex)
|
||||
j = j + #hex
|
||||
else
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
res = res .. escape_char_map_inv[c]
|
||||
end
|
||||
k = j + 1
|
||||
|
||||
elseif x == 34 then -- `"`: End of string
|
||||
res = res .. str:sub(k, j - 1)
|
||||
return res, j + 1
|
||||
end
|
||||
|
||||
j = j + 1
|
||||
end
|
||||
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
local res, idx = parse(str, next_char(str, 1, space_chars, true))
|
||||
idx = next_char(str, idx, space_chars, true)
|
||||
if idx <= #str then
|
||||
decode_error(str, idx, "trailing garbage")
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
return json
|
96
src/lua/libs/print_r.lua
Normal file
96
src/lua/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
|
105
src/lua/libs/randbytes.lua
Normal file
105
src/lua/libs/randbytes.lua
Normal file
@ -0,0 +1,105 @@
|
||||
-- randbytes.lua
|
||||
-- Colin 'Oka' Hall-Coates
|
||||
-- MIT, 2015
|
||||
-- https://github.com/okabsd/randbytes
|
||||
local defaults = setmetatable ({
|
||||
bytes = 4,
|
||||
mask = 256,
|
||||
file = 'urandom',
|
||||
filetable = {'urandom', 'random'}
|
||||
}, { __newindex = function () return false end })
|
||||
|
||||
local files = {
|
||||
urandom = false,
|
||||
random = false
|
||||
}
|
||||
|
||||
local utils = {}
|
||||
|
||||
function utils:gettable (...)
|
||||
local t, r = {...}, defaults.filetable
|
||||
if #t > 0 then r = t end
|
||||
return r
|
||||
end
|
||||
|
||||
function utils:open (...)
|
||||
for _, f in next, self:gettable (...) do
|
||||
for k, _ in next, files do
|
||||
if k == f then
|
||||
files[f] = assert (io.open ('/dev/'..f, 'rb'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function utils:close (...)
|
||||
for _, f in next, self:gettable (...) do
|
||||
for k, _ in next, files do
|
||||
if files[f] and k == f then
|
||||
files[f] = not assert (files[f]:close ())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function utils:reader (f, b)
|
||||
if f then
|
||||
return f:read (b or defaults.bytes)
|
||||
end
|
||||
end
|
||||
|
||||
local randbytes = {
|
||||
generate = function (f, ...)
|
||||
if f then
|
||||
local n, m = 0, select (2, ...) or defaults.mask
|
||||
local s = utils:reader (f, select (1, ...))
|
||||
|
||||
for i = 1, s:len () do
|
||||
n = m * n + s:byte (i)
|
||||
end
|
||||
|
||||
return n
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
function randbytes:open (...)
|
||||
utils:open (...)
|
||||
return self
|
||||
end
|
||||
|
||||
function randbytes:close (...)
|
||||
utils:close (...)
|
||||
return self
|
||||
end
|
||||
|
||||
function randbytes:uread (...)
|
||||
return utils:reader (files.urandom, ...)
|
||||
end
|
||||
|
||||
function randbytes:read (...)
|
||||
return utils:reader (files.random, ...)
|
||||
end
|
||||
|
||||
function randbytes:urandom (...)
|
||||
return self.generate (files.urandom, ...)
|
||||
end
|
||||
|
||||
function randbytes:random (...)
|
||||
return self.generate (files.random, ...)
|
||||
end
|
||||
|
||||
function randbytes:setdefault (k, v)
|
||||
defaults[k] = v or defaults[k]
|
||||
return defaults[k]
|
||||
end
|
||||
|
||||
utils:open ()
|
||||
|
||||
return setmetatable (randbytes, {
|
||||
__call = function (t, ...)
|
||||
return utils:reader (files[defaults.file], ...)
|
||||
end,
|
||||
__metatable = false,
|
||||
__newindex = function () return false end
|
||||
})
|
5675
src/lua/libs/sha.lua
Normal file
5675
src/lua/libs/sha.lua
Normal file
File diff suppressed because it is too large
Load Diff
536
src/lua/libs/url.lua
Normal file
536
src/lua/libs/url.lua
Normal file
@ -0,0 +1,536 @@
|
||||
-- net/url.lua - a robust url parser and builder
|
||||
--
|
||||
-- Bertrand Mansion, 2011-2021; License MIT
|
||||
-- @module net.url
|
||||
-- @alias M
|
||||
|
||||
local M = {}
|
||||
M.version = "1.1.0"
|
||||
|
||||
--- url options
|
||||
-- - `separator` is set to `&` by default but could be anything like `&amp;` or `;`
|
||||
-- - `cumulative_parameters` is false by default. If true, query parameters with the same name will be stored in a table.
|
||||
-- - `legal_in_path` is a table of characters that will not be url encoded in path components
|
||||
-- - `legal_in_query` is a table of characters that will not be url encoded in query values. Query parameters only support a small set of legal characters (-_.).
|
||||
-- - `query_plus_is_space` is true by default, so a plus sign in a query value will be converted to %20 (space), not %2B (plus)
|
||||
-- @todo Add option to limit the size of the argument table
|
||||
-- @todo Add option to limit the depth of the argument table
|
||||
-- @todo Add option to process dots in parameter names, ie. `param.filter=1`
|
||||
M.options = {
|
||||
separator = '&',
|
||||
cumulative_parameters = false,
|
||||
square_bracket_key = false,
|
||||
max_query_parse_keys = 50,
|
||||
max_query_parse_length = 32 * 1024,
|
||||
legal_in_path = {
|
||||
[":"] = true, ["-"] = true, ["_"] = true, ["."] = true,
|
||||
["!"] = true, ["~"] = true, ["*"] = true, ["'"] = true,
|
||||
["("] = true, [")"] = true, ["@"] = true, ["&"] = true,
|
||||
["="] = true, ["$"] = true, [","] = true,
|
||||
[";"] = true
|
||||
},
|
||||
legal_in_query = {
|
||||
[":"] = true, ["-"] = true, ["_"] = true, ["."] = true,
|
||||
[","] = true, ["!"] = true, ["~"] = true, ["*"] = true,
|
||||
["'"] = true, [";"] = true, ["("] = true, [")"] = true,
|
||||
["@"] = true, ["$"] = true,
|
||||
},
|
||||
query_plus_is_space = true
|
||||
}
|
||||
|
||||
--- list of known and common scheme ports
|
||||
-- as documented in <a href="http://www.iana.org/assignments/uri-schemes.html">IANA URI scheme list</a>
|
||||
M.services = {
|
||||
acap = 674,
|
||||
cap = 1026,
|
||||
dict = 2628,
|
||||
ftp = 21,
|
||||
gopher = 70,
|
||||
http = 80,
|
||||
https = 443,
|
||||
iax = 4569,
|
||||
icap = 1344,
|
||||
imap = 143,
|
||||
ipp = 631,
|
||||
ldap = 389,
|
||||
mtqp = 1038,
|
||||
mupdate = 3905,
|
||||
news = 2009,
|
||||
nfs = 2049,
|
||||
nntp = 119,
|
||||
rtsp = 554,
|
||||
sip = 5060,
|
||||
snmp = 161,
|
||||
telnet = 23,
|
||||
tftp = 69,
|
||||
vemmi = 575,
|
||||
afs = 1483,
|
||||
jms = 5673,
|
||||
rsync = 873,
|
||||
prospero = 191,
|
||||
videotex = 516
|
||||
}
|
||||
|
||||
local function decode(str)
|
||||
return (str:gsub("%%(%x%x)", function(c)
|
||||
return string.char(tonumber(c, 16))
|
||||
end))
|
||||
end
|
||||
|
||||
local function encode(str, legal)
|
||||
return (str:gsub("([^%w])", function(v)
|
||||
if legal[v] then
|
||||
return v
|
||||
end
|
||||
return string.upper(string.format("%%%02x", string.byte(v)))
|
||||
end))
|
||||
end
|
||||
|
||||
-- for query values, + can mean space if configured as such
|
||||
local function decodeValue(str)
|
||||
if M.options.query_plus_is_space then
|
||||
str = str:gsub('+', ' ')
|
||||
end
|
||||
return decode(str)
|
||||
end
|
||||
|
||||
local function concat(a, b)
|
||||
if type(a) == 'table' then
|
||||
return a:build() .. b
|
||||
else
|
||||
return a .. b:build()
|
||||
end
|
||||
end
|
||||
|
||||
function M:addSegment(path)
|
||||
if type(path) == 'string' then
|
||||
self.path = self.path .. '/' .. encode(path:gsub("^/+", ""), M.options.legal_in_path)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- builds the url
|
||||
-- @return a string representing the built url
|
||||
function M:build()
|
||||
local url = ''
|
||||
if self.path then
|
||||
local path = self.path
|
||||
url = url .. tostring(path)
|
||||
end
|
||||
if self.query then
|
||||
local qstring = tostring(self.query)
|
||||
if qstring ~= "" then
|
||||
url = url .. '?' .. qstring
|
||||
end
|
||||
end
|
||||
if self.host then
|
||||
local authority = self.host
|
||||
if self.port and self.scheme and M.services[self.scheme] ~= self.port then
|
||||
authority = authority .. ':' .. self.port
|
||||
end
|
||||
local userinfo
|
||||
if self.user and self.user ~= "" then
|
||||
userinfo = self.user
|
||||
if self.password then
|
||||
userinfo = userinfo .. ':' .. self.password
|
||||
end
|
||||
end
|
||||
if userinfo and userinfo ~= "" then
|
||||
authority = userinfo .. '@' .. authority
|
||||
end
|
||||
if authority then
|
||||
if url ~= "" then
|
||||
url = '//' .. authority .. '/' .. url:gsub('^/+', '')
|
||||
else
|
||||
url = '//' .. authority
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.scheme then
|
||||
url = self.scheme .. ':' .. url
|
||||
end
|
||||
if self.fragment then
|
||||
url = url .. '#' .. self.fragment
|
||||
end
|
||||
return url
|
||||
end
|
||||
|
||||
--- builds the querystring
|
||||
-- @param tab The key/value parameters
|
||||
-- @param sep The separator to use (optional)
|
||||
-- @param key The parent key if the value is multi-dimensional (optional)
|
||||
-- @return a string representing the built querystring
|
||||
function M.buildQuery(tab, sep, key)
|
||||
local query = {}
|
||||
if not sep then
|
||||
sep = M.options.separator or '&'
|
||||
end
|
||||
local keys = {}
|
||||
for k in pairs(tab) do
|
||||
keys[#keys+1] = k
|
||||
end
|
||||
table.sort(keys, function (a, b)
|
||||
local function padnum(n, rest) return ("%03d"..rest):format(tonumber(n)) end
|
||||
return tostring(a):gsub("(%d+)(%.)",padnum) < tostring(b):gsub("(%d+)(%.)",padnum)
|
||||
end)
|
||||
for _,name in ipairs(keys) do
|
||||
local value = tab[name]
|
||||
name = encode(tostring(name), {["-"] = true, ["_"] = true, ["."] = true})
|
||||
if key then
|
||||
if M.options.cumulative_parameters and string.find(name, '^%d+$') then
|
||||
name = tostring(key)
|
||||
else
|
||||
name = string.format('%s[%s]', tostring(key), tostring(name))
|
||||
end
|
||||
end
|
||||
if type(value) == 'table' then
|
||||
query[#query+1] = M.buildQuery(value, sep, name)
|
||||
else
|
||||
local value = encode(tostring(value), M.options.legal_in_query)
|
||||
if value ~= "" then
|
||||
query[#query+1] = string.format('%s=%s', name, value)
|
||||
else
|
||||
query[#query+1] = name
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(query, sep)
|
||||
end
|
||||
|
||||
--- Parses the querystring to a table
|
||||
-- This function can parse multidimensional pairs and is mostly compatible
|
||||
-- with PHP usage of brackets in key names like ?param[key]=value
|
||||
-- @param str The querystring to parse
|
||||
-- @param sep The separator between key/value pairs, defaults to `&`
|
||||
-- @todo limit the max number of parameters with M.options.max_parameters
|
||||
-- @return a table representing the query key/value pairs
|
||||
function M.parseQuery(str, sep)
|
||||
if #str > M.options.max_query_parse_length then
|
||||
return
|
||||
end
|
||||
|
||||
if not sep then
|
||||
sep = M.options.separator or '&'
|
||||
end
|
||||
|
||||
local values = {}
|
||||
local parts = 0
|
||||
for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do
|
||||
if parts > M.options.max_query_parse_keys then
|
||||
break
|
||||
end
|
||||
local key = decodeValue(key)
|
||||
local keys = {}
|
||||
if M.options.square_bracket_key then
|
||||
key = key:gsub('%[([^%]]*)%]', function(v)
|
||||
-- extract keys between balanced brackets
|
||||
if string.find(v, "^-?%d+$") then
|
||||
v = tonumber(v)
|
||||
else
|
||||
v = decodeValue(v)
|
||||
end
|
||||
table.insert(keys, v)
|
||||
return "="
|
||||
end)
|
||||
end
|
||||
key = key:gsub('=+.*$', "")
|
||||
key = key:gsub('%s', "_") -- remove spaces in parameter name
|
||||
val = val:gsub('^=+', "")
|
||||
|
||||
if not values[key] then
|
||||
values[key] = {}
|
||||
end
|
||||
if #keys > 0 and type(values[key]) ~= 'table' then
|
||||
values[key] = {}
|
||||
elseif #keys == 0 and type(values[key]) == 'table' then
|
||||
values[key] = decodeValue(val)
|
||||
elseif M.options.cumulative_parameters
|
||||
and type(values[key]) == 'string' then
|
||||
values[key] = { values[key] }
|
||||
table.insert(values[key], decodeValue(val))
|
||||
end
|
||||
|
||||
local t = values[key]
|
||||
for i,k in ipairs(keys) do
|
||||
if type(t) ~= 'table' then
|
||||
t = {}
|
||||
end
|
||||
if k == "" then
|
||||
k = #t+1
|
||||
end
|
||||
if not t[k] then
|
||||
t[k] = {}
|
||||
end
|
||||
if i == #keys then
|
||||
t[k] = val
|
||||
end
|
||||
t = t[k]
|
||||
end
|
||||
parts = parts + 1
|
||||
end
|
||||
setmetatable(values, { __tostring = M.buildQuery })
|
||||
return values
|
||||
end
|
||||
|
||||
--- set the url query
|
||||
-- @param query Can be a string to parse or a table of key/value pairs
|
||||
-- @return a table representing the query key/value pairs
|
||||
function M:setQuery(query)
|
||||
local query = query
|
||||
if type(query) == 'table' then
|
||||
query = M.buildQuery(query)
|
||||
end
|
||||
self.query = M.parseQuery(query)
|
||||
return query
|
||||
end
|
||||
|
||||
--- set the authority part of the url
|
||||
-- The authority is parsed to find the user, password, port and host if available.
|
||||
-- @param authority The string representing the authority
|
||||
-- @return a string with what remains after the authority was parsed
|
||||
function M:setAuthority(authority)
|
||||
self.authority = authority
|
||||
self.port = nil
|
||||
self.host = nil
|
||||
self.userinfo = nil
|
||||
self.user = nil
|
||||
self.password = nil
|
||||
|
||||
authority = authority:gsub('^([^@]*)@', function(v)
|
||||
self.userinfo = v
|
||||
return ''
|
||||
end)
|
||||
|
||||
authority = authority:gsub(':(%d+)$', function(v)
|
||||
self.port = tonumber(v)
|
||||
return ''
|
||||
end)
|
||||
|
||||
local function getIP(str)
|
||||
-- ipv4
|
||||
local chunks = { str:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") }
|
||||
if #chunks == 4 then
|
||||
for _, v in pairs(chunks) do
|
||||
if tonumber(v) > 255 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
-- ipv6
|
||||
local chunks = { str:match("^%["..(("([a-fA-F0-9]*):"):rep(8):gsub(":$","%%]$"))) }
|
||||
if #chunks == 8 or #chunks < 8 and
|
||||
str:match('::') and not str:gsub("::", "", 1):match('::') then
|
||||
for _,v in pairs(chunks) do
|
||||
if #v > 0 and tonumber(v, 16) > 65535 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local ip = getIP(authority)
|
||||
if ip then
|
||||
self.host = ip
|
||||
elseif type(ip) == 'nil' then
|
||||
-- domain
|
||||
if authority ~= '' and not self.host then
|
||||
local host = authority:lower()
|
||||
if string.match(host, '^[%d%a%-%.]+$') ~= nil and
|
||||
string.sub(host, 0, 1) ~= '.' and
|
||||
string.sub(host, -1) ~= '.' and
|
||||
string.find(host, '%.%.') == nil then
|
||||
self.host = host
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.userinfo then
|
||||
local userinfo = self.userinfo
|
||||
userinfo = userinfo:gsub(':([^:]*)$', function(v)
|
||||
self.password = v
|
||||
return ''
|
||||
end)
|
||||
if string.find(userinfo, "^[%w%+%.]+$") then
|
||||
self.user = userinfo
|
||||
else
|
||||
-- incorrect userinfo
|
||||
self.userinfo = nil
|
||||
self.user = nil
|
||||
self.password = nil
|
||||
end
|
||||
end
|
||||
|
||||
return authority
|
||||
end
|
||||
|
||||
--- Parse the url into the designated parts.
|
||||
-- Depending on the url, the following parts can be available:
|
||||
-- scheme, userinfo, user, password, authority, host, port, path,
|
||||
-- query, fragment
|
||||
-- @param url Url string
|
||||
-- @return a table with the different parts and a few other functions
|
||||
function M.parse(url)
|
||||
local comp = {}
|
||||
M.setAuthority(comp, "")
|
||||
M.setQuery(comp, "")
|
||||
|
||||
local url = tostring(url or '')
|
||||
url = url:gsub('#(.*)$', function(v)
|
||||
comp.fragment = v
|
||||
return ''
|
||||
end)
|
||||
url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v)
|
||||
comp.scheme = v:lower()
|
||||
return ''
|
||||
end)
|
||||
url = url:gsub('%?(.*)', function(v)
|
||||
M.setQuery(comp, v)
|
||||
return ''
|
||||
end)
|
||||
url = url:gsub('^//([^/]*)', function(v)
|
||||
M.setAuthority(comp, v)
|
||||
return ''
|
||||
end)
|
||||
|
||||
comp.path = url:gsub("([^/]+)", function (s) return encode(decode(s), M.options.legal_in_path) end)
|
||||
|
||||
setmetatable(comp, {
|
||||
__index = M,
|
||||
__tostring = M.build,
|
||||
__concat = concat,
|
||||
__div = M.addSegment
|
||||
})
|
||||
return comp
|
||||
end
|
||||
|
||||
--- removes dots and slashes in urls when possible
|
||||
-- This function will also remove multiple slashes
|
||||
-- @param path The string representing the path to clean
|
||||
-- @return a string of the path without unnecessary dots and segments
|
||||
function M.removeDotSegments(path)
|
||||
local fields = {}
|
||||
if string.len(path) == 0 then
|
||||
return ""
|
||||
end
|
||||
local startslash = false
|
||||
local endslash = false
|
||||
if string.sub(path, 1, 1) == "/" then
|
||||
startslash = true
|
||||
end
|
||||
if (string.len(path) > 1 or startslash == false) and string.sub(path, -1) == "/" then
|
||||
endslash = true
|
||||
end
|
||||
|
||||
path:gsub('[^/]+', function(c) table.insert(fields, c) end)
|
||||
|
||||
local new = {}
|
||||
local j = 0
|
||||
|
||||
for i,c in ipairs(fields) do
|
||||
if c == '..' then
|
||||
if j > 0 then
|
||||
j = j - 1
|
||||
end
|
||||
elseif c ~= "." then
|
||||
j = j + 1
|
||||
new[j] = c
|
||||
end
|
||||
end
|
||||
local ret = ""
|
||||
if #new > 0 and j > 0 then
|
||||
ret = table.concat(new, '/', 1, j)
|
||||
else
|
||||
ret = ""
|
||||
end
|
||||
if startslash then
|
||||
ret = '/'..ret
|
||||
end
|
||||
if endslash then
|
||||
ret = ret..'/'
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function reducePath(base_path, relative_path)
|
||||
if string.sub(relative_path, 1, 1) == "/" then
|
||||
return '/' .. string.gsub(relative_path, '^[%./]+', '')
|
||||
end
|
||||
local path = base_path
|
||||
local startslash = string.sub(path, 1, 1) ~= "/";
|
||||
if relative_path ~= "" then
|
||||
path = (startslash and '' or '/') .. path:gsub("[^/]*$", "")
|
||||
end
|
||||
path = path .. relative_path
|
||||
path = path:gsub("([^/]*%./)", function (s)
|
||||
if s ~= "./" then return s else return "" end
|
||||
end)
|
||||
path = string.gsub(path, "/%.$", "/")
|
||||
local reduced
|
||||
while reduced ~= path do
|
||||
reduced = path
|
||||
path = string.gsub(reduced, "([^/]*/%.%./)", function (s)
|
||||
if s ~= "../../" then return "" else return s end
|
||||
end)
|
||||
end
|
||||
path = string.gsub(path, "([^/]*/%.%.?)$", function (s)
|
||||
if s ~= "../.." then return "" else return s end
|
||||
end)
|
||||
local reduced
|
||||
while reduced ~= path do
|
||||
reduced = path
|
||||
path = string.gsub(reduced, '^/?%.%./', '')
|
||||
end
|
||||
return (startslash and '' or '/') .. path
|
||||
end
|
||||
|
||||
--- builds a new url by using the one given as parameter and resolving paths
|
||||
-- @param other A string or a table representing a url
|
||||
-- @return a new url table
|
||||
function M:resolve(other)
|
||||
if type(self) == "string" then
|
||||
self = M.parse(self)
|
||||
end
|
||||
if type(other) == "string" then
|
||||
other = M.parse(other)
|
||||
end
|
||||
if other.scheme then
|
||||
return other
|
||||
else
|
||||
other.scheme = self.scheme
|
||||
if not other.authority or other.authority == "" then
|
||||
other:setAuthority(self.authority)
|
||||
if not other.path or other.path == "" then
|
||||
other.path = self.path
|
||||
local query = other.query
|
||||
if not query or not next(query) then
|
||||
other.query = self.query
|
||||
end
|
||||
else
|
||||
other.path = reducePath(self.path, other.path)
|
||||
end
|
||||
end
|
||||
return other
|
||||
end
|
||||
end
|
||||
|
||||
--- normalize a url path following some common normalization rules
|
||||
-- described on <a href="http://en.wikipedia.org/wiki/URL_normalization">The URL normalization page of Wikipedia</a>
|
||||
-- @return the normalized path
|
||||
function M:normalize()
|
||||
if type(self) == 'string' then
|
||||
self = M.parse(self)
|
||||
end
|
||||
if self.path then
|
||||
local path = self.path
|
||||
path = reducePath(path, "")
|
||||
-- normalize multiple slashes
|
||||
path = string.gsub(path, "//+", "/")
|
||||
self.path = path
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return M
|
68
src/lua/libs/utils.lua
Normal file
68
src/lua/libs/utils.lua
Normal file
@ -0,0 +1,68 @@
|
||||
local _M = {}
|
||||
|
||||
local sha = require("sha")
|
||||
local secret_bucket_duration = tonumber(os.getenv("BUCKET_DURATION"))
|
||||
local challenge_includes_ip = os.getenv("CHALLENGE_INCLUDES_IP")
|
||||
local tor_control_port_password = os.getenv("TOR_CONTROL_PORT_PASSWORD")
|
||||
|
||||
-- generate the challenge hash/user hash
|
||||
function _M.generate_secret(context, salt, user_key, is_applet)
|
||||
|
||||
-- time bucket for expiry
|
||||
local start_sec = core.now()['sec']
|
||||
local bucket = start_sec - (start_sec % secret_bucket_duration)
|
||||
|
||||
-- optional IP to lock challenges/user_keys to IP (for clearnet or single-onion aka 99% of cases)
|
||||
local ip = ""
|
||||
if challenge_includes_ip == "1" then
|
||||
ip = context.sf:src()
|
||||
end
|
||||
|
||||
-- user agent to counter very dumb spammers
|
||||
local user_agent = ""
|
||||
if is_applet == true then
|
||||
user_agent = context.headers['user-agent'] or {}
|
||||
user_agent = user_agent[0] or ""
|
||||
else
|
||||
--note req_fhdr not req_hdr otherwise commas in useragent become a delimiter
|
||||
user_agent = context.sf:req_fhdr('user-agent') or ""
|
||||
end
|
||||
|
||||
return sha.sha3_256(salt .. bucket .. ip .. user_key .. user_agent)
|
||||
|
||||
end
|
||||
|
||||
-- split string by delimiter
|
||||
function _M.split(inputstr, sep)
|
||||
local t = {}
|
||||
for str in string.gmatch(inputstr, "([^"..sep.."]*)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-- return true if hash passes difficulty
|
||||
function _M.checkdiff(hash, diff)
|
||||
local i = 1
|
||||
for j = 0, (diff-8), 8 do
|
||||
if hash:sub(i, i) ~= "0" then
|
||||
return false
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
local lnm = tonumber(hash:sub(i, i), 16)
|
||||
local msk = 0xff >> ((i*8)-diff)
|
||||
return (lnm & msk) == 0
|
||||
end
|
||||
|
||||
-- connect to the tor control port and instruct it to close a circuit
|
||||
function _M.send_tor_control_port(circuit_identifier)
|
||||
local tcp = core.tcp();
|
||||
tcp:settimeout(1);
|
||||
tcp:connect("127.0.0.1", 9051);
|
||||
-- not buffered, so we are better off sending it all at once
|
||||
tcp:send('AUTHENTICATE "' .. tor_control_port_password .. '"\nCLOSECIRCUIT ' .. circuit_identifier ..'\n')
|
||||
tcp:close()
|
||||
end
|
||||
|
||||
return _M
|
358
src/lua/scripts/bot-check.lua
Normal file
358
src/lua/scripts/bot-check.lua
Normal file
@ -0,0 +1,358 @@
|
||||
_M = {}
|
||||
|
||||
-- Testing only
|
||||
-- require("socket")
|
||||
-- require("print_r")
|
||||
|
||||
-- main libs
|
||||
local url = require("url")
|
||||
local utils = require("utils")
|
||||
local cookie = require("cookie")
|
||||
local json = require("json")
|
||||
local sha = require("sha")
|
||||
local randbytes = require("randbytes")
|
||||
local templates = require("templates")
|
||||
|
||||
-- POW
|
||||
local pow_difficulty = tonumber(os.getenv("POW_DIFFICULTY") or 18)
|
||||
|
||||
-- argon2
|
||||
local argon2 = require("argon2")
|
||||
local argon_kb = tonumber(os.getenv("ARGON_KB") or 6000)
|
||||
local argon_time = tonumber(os.getenv("ARGON_TIME") or 1)
|
||||
argon2.t_cost(argon_time)
|
||||
argon2.m_cost(argon_kb)
|
||||
argon2.parallelism(1)
|
||||
argon2.hash_len(32)
|
||||
argon2.variant(argon2.variants.argon2_id)
|
||||
|
||||
-- sha2
|
||||
-- TODO
|
||||
|
||||
-- environment variables
|
||||
local captcha_secret = os.getenv("HCAPTCHA_SECRET") or os.getenv("RECAPTCHA_SECRET")
|
||||
local captcha_sitekey = os.getenv("HCAPTCHA_SITEKEY") or os.getenv("RECAPTCHA_SITEKEY")
|
||||
local captcha_cookie_secret = os.getenv("CAPTCHA_COOKIE_SECRET")
|
||||
local pow_cookie_secret = os.getenv("POW_COOKIE_SECRET")
|
||||
local hmac_cookie_secret = os.getenv("HMAC_COOKIE_SECRET")
|
||||
local ray_id = os.getenv("RAY_ID")
|
||||
|
||||
-- load captcha map and set hcaptcha/recaptch based off env vars
|
||||
local captcha_map = Map.new("/etc/haproxy/ddos.map", Map._str);
|
||||
local captcha_provider_domain = ""
|
||||
local captcha_classname = ""
|
||||
local captcha_script_src = ""
|
||||
local captcha_siteverify_path = ""
|
||||
local captcha_backend_name = ""
|
||||
if os.getenv("HCAPTCHA_SITEKEY") then
|
||||
captcha_provider_domain = "hcaptcha.com"
|
||||
captcha_classname = "h-captcha"
|
||||
captcha_script_src = "https://hcaptcha.com/1/api.js"
|
||||
captcha_siteverify_path = "/siteverify"
|
||||
captcha_backend_name = "hcaptcha"
|
||||
else
|
||||
captcha_provider_domain = "www.google.com"
|
||||
captcha_classname = "g-recaptcha"
|
||||
captcha_script_src = "https://www.google.com/recaptcha/api.js"
|
||||
captcha_siteverify_path = "/recaptcha/api/siteverify"
|
||||
captcha_backend_name = "recaptcha"
|
||||
end
|
||||
|
||||
-- setup initial server backends based on hosts.map into backends.map
|
||||
function _M.setup_servers()
|
||||
if pow_difficulty < 8 then
|
||||
error("POW_DIFFICULTY must be > 8. Around 16-32 is better")
|
||||
end
|
||||
local backend_name = os.getenv("BACKEND_NAME")
|
||||
local server_prefix = os.getenv("SERVER_PREFIX")
|
||||
if backend_name == nil or server_prefix == nil then
|
||||
return;
|
||||
end
|
||||
local hosts_map = Map.new("/etc/haproxy/hosts.map", Map._str);
|
||||
local handle = io.open("/etc/haproxy/hosts.map", "r")
|
||||
local line = handle:read("*line")
|
||||
local counter = 1
|
||||
while line do
|
||||
local domain, backend_host = line:match("([^%s]+)%s+([^%s]+)")
|
||||
local port_index = backend_host:match'^.*():'
|
||||
local backend_hostname = backend_host:sub(0, port_index-1)
|
||||
local backend_port = backend_host:sub(port_index + 1)
|
||||
core.set_map("/etc/haproxy/backends.map", domain, server_prefix..counter)
|
||||
local proxy = core.proxies[backend_name].servers[server_prefix..counter]
|
||||
proxy:set_addr(backend_hostname, backend_port)
|
||||
proxy:set_ready()
|
||||
line = handle:read("*line")
|
||||
counter = counter + 1
|
||||
end
|
||||
handle:close()
|
||||
end
|
||||
|
||||
-- kill a tor circuit
|
||||
function _M.kill_tor_circuit(txn)
|
||||
local ip = txn.sf:src()
|
||||
if ip:sub(1,19) ~= "fc00:dead:beef:4dad" then
|
||||
return -- not a tor circuit id/ip. we shouldn't get here, but just in case.
|
||||
end
|
||||
-- split the IP, take the last 2 sections
|
||||
local split_ip = utils.split(ip, ":")
|
||||
local aa_bb = split_ip[5] or "0000"
|
||||
local cc_dd = split_ip[6] or "0000"
|
||||
aa_bb = string.rep("0", 4 - #aa_bb) .. aa_bb
|
||||
cc_dd = string.rep("0", 4 - #cc_dd) .. cc_dd
|
||||
-- convert the last 2 sections to a number from hex, which makes the circuit ID
|
||||
local circuit_identifier = tonumber(aa_bb..cc_dd, 16)
|
||||
print('Closing Tor circuit ID: '..circuit_identifier..', "IP": '..ip)
|
||||
utils.send_tor_control_port(circuit_identifier)
|
||||
end
|
||||
|
||||
function _M.view(applet)
|
||||
|
||||
-- set response body and declare status code
|
||||
local response_body = ""
|
||||
local response_status_code
|
||||
|
||||
-- if request is GET, serve the challenge page
|
||||
if applet.method == "GET" then
|
||||
|
||||
-- get the user_key#challenge#sig
|
||||
local user_key = sha.bin_to_hex(randbytes(16))
|
||||
local challenge_hash = utils.generate_secret(applet, pow_cookie_secret, user_key, true)
|
||||
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. challenge_hash)
|
||||
local combined_challenge = user_key .. "#" .. challenge_hash .. "#" .. signature
|
||||
|
||||
-- define body sections
|
||||
local site_name_body = ""
|
||||
local captcha_body = ""
|
||||
local pow_body = ""
|
||||
local noscript_extra_body = ""
|
||||
|
||||
-- check if captcha is enabled, path+domain priority, then just domain, and 0 otherwise
|
||||
local captcha_enabled = false
|
||||
local host = applet.headers['host'][0]
|
||||
local path = applet.qs; --because on /.basedflare/bot-check?/whatever, .qs (query string) holds the "path"
|
||||
|
||||
local captcha_map_lookup = captcha_map:lookup(host..path) or captcha_map:lookup(host) or 0
|
||||
captcha_map_lookup = tonumber(captcha_map_lookup)
|
||||
if captcha_map_lookup == 2 then
|
||||
captcha_enabled = true
|
||||
end
|
||||
|
||||
-- pow at least is always enabled when reaching bot-check page
|
||||
site_name_body = string.format(templates.site_name_section, host)
|
||||
if captcha_enabled then
|
||||
captcha_body = string.format(templates.captcha_section, captcha_classname,
|
||||
captcha_sitekey, captcha_script_src)
|
||||
else
|
||||
pow_body = templates.pow_section
|
||||
noscript_extra_body = string.format(templates.noscript_extra, user_key, challenge_hash, signature,
|
||||
math.ceil(pow_difficulty/8), argon_time, argon_kb)
|
||||
end
|
||||
|
||||
-- sub in the body sections
|
||||
response_body = string.format(templates.body, combined_challenge,
|
||||
pow_difficulty, argon_time, argon_kb,
|
||||
site_name_body, pow_body, captcha_body, noscript_extra_body, ray_id)
|
||||
response_status_code = 403
|
||||
|
||||
-- if request is POST, check the answer to the pow/cookie
|
||||
elseif applet.method == "POST" then
|
||||
|
||||
-- if they fail, set a var for use in ACLs later
|
||||
local valid_submission = false
|
||||
|
||||
-- parsed POST body
|
||||
local parsed_body = url.parseQuery(applet.receive(applet))
|
||||
|
||||
-- whether to set cookies sent as secure or not
|
||||
local secure_cookie_flag = " Secure=true;"
|
||||
if applet.sf:ssl_fc() == "0" then
|
||||
secure_cookie_flag = ""
|
||||
end
|
||||
|
||||
-- handle setting the POW cookie
|
||||
local user_pow_response = parsed_body["pow_response"]
|
||||
if user_pow_response then
|
||||
|
||||
-- split the response up (makes the nojs submission easier because it can be a single field)
|
||||
local split_response = utils.split(user_pow_response, "#")
|
||||
|
||||
if #split_response == 4 then
|
||||
local given_user_key = split_response[1]
|
||||
local given_challenge_hash = split_response[2]
|
||||
local given_signature = split_response[3]
|
||||
local given_answer = split_response[4]
|
||||
|
||||
-- regenerate the challenge and compare it
|
||||
local generated_challenge_hash = utils.generate_secret(applet, pow_cookie_secret, given_user_key, true)
|
||||
if given_challenge_hash == generated_challenge_hash then
|
||||
|
||||
-- regenerate the signature and compare it
|
||||
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash)
|
||||
if given_signature == generated_signature then
|
||||
|
||||
-- do the work with their given answer
|
||||
local full_hash = argon2.hash_encoded(given_challenge_hash .. given_answer, given_user_key)
|
||||
|
||||
-- check the output is correct
|
||||
local hash_output = utils.split(full_hash, '$')[6]:sub(0, 43) -- https://github.com/thibaultcha/lua-argon2/issues/37
|
||||
local hex_hash_output = sha.bin_to_hex(sha.base64_to_bin(hash_output));
|
||||
if utils.checkdiff(hex_hash_output, pow_difficulty) then
|
||||
|
||||
-- the answer was good, give them a cookie
|
||||
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
|
||||
local combined_cookie = given_user_key .. "#" .. given_challenge_hash .. "#" .. given_answer .. "#" .. signature
|
||||
applet:add_header(
|
||||
"set-cookie",
|
||||
string.format(
|
||||
"z_ddos_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
|
||||
combined_cookie,
|
||||
applet.headers['host'][0],
|
||||
secure_cookie_flag
|
||||
)
|
||||
)
|
||||
valid_submission = true
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle setting the captcha cookie
|
||||
local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"]
|
||||
if valid_submission and user_captcha_response then -- only check captcha if POW is already correct
|
||||
-- format the url for verifying the captcha response
|
||||
local captcha_url = string.format(
|
||||
"https://%s%s",
|
||||
core.backends[captcha_backend_name].servers[captcha_backend_name]:get_addr(),
|
||||
captcha_siteverify_path
|
||||
)
|
||||
-- construct the captcha body to send to the captcha url
|
||||
local captcha_body = url.buildQuery({
|
||||
secret=captcha_secret,
|
||||
response=user_captcha_response
|
||||
})
|
||||
-- instantiate an http client and make the request
|
||||
local httpclient = core.httpclient()
|
||||
local res = httpclient:post{
|
||||
url=captcha_url,
|
||||
body=captcha_body,
|
||||
headers={
|
||||
[ "host" ] = { captcha_provider_domain },
|
||||
[ "content-type" ] = { "application/x-www-form-urlencoded" }
|
||||
}
|
||||
}
|
||||
-- try parsing the response as json
|
||||
local status, api_response = pcall(json.decode, res.body)
|
||||
if not status then
|
||||
api_response = {}
|
||||
end
|
||||
-- the response was good i.e the captcha provider says they passed, give them a cookie
|
||||
if api_response.success == true then
|
||||
|
||||
local user_key = sha.bin_to_hex(randbytes(16))
|
||||
local user_hash = utils.generate_secret(applet, captcha_cookie_secret, user_key, true)
|
||||
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, user_key .. user_hash)
|
||||
local combined_cookie = user_key .. "#" .. user_hash .. "#" .. signature
|
||||
applet:add_header(
|
||||
"set-cookie",
|
||||
string.format(
|
||||
"z_ddos_captcha=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
|
||||
combined_cookie,
|
||||
applet.headers['host'][0],
|
||||
secure_cookie_flag
|
||||
)
|
||||
)
|
||||
valid_submission = valid_submission and true
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
if not valid_submission then
|
||||
_M.kill_tor_circuit(applet)
|
||||
end
|
||||
|
||||
-- redirect them to their desired page in applet.qs (query string)
|
||||
-- if they didn't get the appropriate cookies they will be sent back to the challenge page
|
||||
response_status_code = 302
|
||||
applet:add_header("location", applet.qs)
|
||||
|
||||
-- else if its another http method, just 403 them
|
||||
else
|
||||
response_status_code = 403
|
||||
end
|
||||
|
||||
-- finish sending the response
|
||||
applet:set_status(response_status_code)
|
||||
applet:add_header("content-type", "text/html; charset=utf-8")
|
||||
applet:add_header("content-length", string.len(response_body))
|
||||
applet:start_response()
|
||||
applet:send(response_body)
|
||||
|
||||
end
|
||||
|
||||
-- check if captcha is enabled, path+domain priority, then just domain, and 0 otherwise
|
||||
function _M.decide_checks_necessary(txn)
|
||||
local host = txn.sf:hdr("Host")
|
||||
local path = txn.sf:path();
|
||||
local captcha_map_lookup = captcha_map:lookup(host..path) or captcha_map:lookup(host) or 0
|
||||
captcha_map_lookup = tonumber(captcha_map_lookup)
|
||||
if captcha_map_lookup == 1 then
|
||||
txn:set_var("txn.validate_pow", true)
|
||||
elseif captcha_map_lookup == 2 then
|
||||
txn:set_var("txn.validate_captcha", true)
|
||||
txn:set_var("txn.validate_pow", true)
|
||||
end
|
||||
-- otherwise, domain+path was set to 0 (whitelist) or there is no entry in the map
|
||||
end
|
||||
|
||||
-- check if captcha cookie is valid, separate secret from POW
|
||||
function _M.check_captcha_status(txn)
|
||||
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
|
||||
local received_captcha_cookie = parsed_request_cookies["z_ddos_captcha"] or ""
|
||||
-- split the cookie up
|
||||
local split_cookie = utils.split(received_captcha_cookie, "#")
|
||||
if #split_cookie ~= 3 then
|
||||
return
|
||||
end
|
||||
local given_user_key = split_cookie[1]
|
||||
local given_user_hash = split_cookie[2]
|
||||
local given_signature = split_cookie[3]
|
||||
-- regenerate the user hash and compare it
|
||||
local generated_user_hash = utils.generate_secret(txn, captcha_cookie_secret, given_user_key, false)
|
||||
if generated_user_hash ~= given_user_hash then
|
||||
return
|
||||
end
|
||||
-- regenerate the signature and compare it
|
||||
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_user_hash)
|
||||
if given_signature == generated_signature then
|
||||
return txn:set_var("txn.captcha_passed", true)
|
||||
end
|
||||
end
|
||||
|
||||
-- check if pow cookie is valid
|
||||
function _M.check_pow_status(txn)
|
||||
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
|
||||
local received_pow_cookie = parsed_request_cookies["z_ddos_pow"] or ""
|
||||
-- split the cookie up
|
||||
local split_cookie = utils.split(received_pow_cookie, "#")
|
||||
if #split_cookie ~= 4 then
|
||||
return
|
||||
end
|
||||
local given_user_key = split_cookie[1]
|
||||
local given_challenge_hash = split_cookie[2]
|
||||
local given_answer = split_cookie[3]
|
||||
local given_signature = split_cookie[4]
|
||||
-- regenerate the challenge and compare it
|
||||
local generated_challenge_hash = utils.generate_secret(txn, pow_cookie_secret, given_user_key, false)
|
||||
if given_challenge_hash ~= generated_challenge_hash then
|
||||
return
|
||||
end
|
||||
-- regenerate the signature and compare it
|
||||
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
|
||||
if given_signature == generated_signature then
|
||||
return txn:set_var("txn.pow_passed", true)
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
10
src/lua/scripts/register.lua
Normal file
10
src/lua/scripts/register.lua
Normal file
@ -0,0 +1,10 @@
|
||||
package.path = package.path .. "./?.lua;/etc/haproxy/scripts/?.lua;/etc/haproxy/libs/?.lua"
|
||||
|
||||
local bot_check = require("bot-check")
|
||||
|
||||
core.register_service("bot-check", "http", bot_check.view)
|
||||
core.register_action("captcha-check", { 'http-req', }, bot_check.check_captcha_status)
|
||||
core.register_action("pow-check", { 'http-req', }, bot_check.check_pow_status)
|
||||
core.register_action("decide-checks-necessary", { 'http-req', }, bot_check.decide_checks_necessary)
|
||||
core.register_action("kill-tor-circuit", { 'http-req', }, bot_check.kill_tor_circuit)
|
||||
core.register_init(bot_check.setup_servers)
|
100
src/lua/scripts/templates.lua
Normal file
100
src/lua/scripts/templates.lua
Normal file
@ -0,0 +1,100 @@
|
||||
local _M = {}
|
||||
|
||||
-- main page template
|
||||
_M.body = [[
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name='viewport' content='width=device-width initial-scale=1'>
|
||||
<title>Hold on...</title>
|
||||
<style>
|
||||
:root{--text-color:#c5c8c6;--bg-color:#1d1f21}
|
||||
@media (prefers-color-scheme:light){:root{--text-color:#333;--bg-color:#EEE}}
|
||||
.h-captcha,.g-recaptcha{min-height:85px;display:block}
|
||||
.red{color:red;font-weight:bold}
|
||||
.powstatus{color:green;font-weight:bold}
|
||||
a,a:visited{color:var(--text-color)}
|
||||
body,html{height:100%%}
|
||||
body{display:flex;flex-direction:column;background-color:var(--bg-color);color:var(--text-color);font-family:Helvetica,Arial,sans-serif;max-width:1200px;margin:0 auto;padding: 0 20px}
|
||||
details{transition: border-left-color 0.5s;max-width:1200px;text-align:left;border-left: 2px solid var(--text-color);padding:10px}
|
||||
code{background-color:#dfdfdf30;border-radius:3px;padding:0 3px;}
|
||||
img,h3,p{margin:0 0 5px 0}
|
||||
footer{font-size:x-small;margin-top:auto;margin-bottom:20px;text-align:center}
|
||||
img{display:inline}
|
||||
.pt{padding-top:15vh;display:flex;align-items:center;word-break:break-all}
|
||||
.pt img{margin-right:10px}
|
||||
details[open]{border-left-color: #1400ff}
|
||||
.lds-ring{display:inline-block;position:relative;width:80px;height:80px}.lds-ring div{box-sizing:border-box;display:block;position:absolute;width:32px;height:32px;margin:10px;border:5px solid var(--text-color);border-radius:50%%;animation:lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;border-color:var(--text-color) transparent transparent transparent}.lds-ring div:nth-child(1){animation-delay:-0.45s}.lds-ring div:nth-child(2){animation-delay:-0.3s}.lds-ring div:nth-child(3){animation-delay:-0.15s}@keyframes lds-ring{0%%{transform:rotate(0deg)}100%%{transform:rotate(360deg)}}
|
||||
</style>
|
||||
<noscript>
|
||||
<style>.jsonly{display:none}</style>
|
||||
</noscript>
|
||||
<script src="/.basedflare/js/argon2.js"></script>
|
||||
<script src="/.basedflare/js/challenge.js"></script>
|
||||
</head>
|
||||
<body data-pow="%s" data-diff="%s" data-time="%s" data-kb="%s">
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
<noscript>
|
||||
<br>
|
||||
<p class="red">JavaScript is required on this page.</p>
|
||||
%s
|
||||
</noscript>
|
||||
<div class="powstatus"></div>
|
||||
<footer>
|
||||
<p>Security and Performance by <a href="https://gitgud.io/fatchan/haproxy-protection/">haproxy-protection</a></p>
|
||||
<p>Node: <code>%s</code></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
]]
|
||||
|
||||
_M.noscript_extra = [[
|
||||
<details>
|
||||
<summary>No JavaScript?</summary>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Run this in a linux terminal (requires <code>argon2</code> package installed):</p>
|
||||
<code style="word-break: break-all;">
|
||||
echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s
|
||||
</code>
|
||||
<li>Paste the script output into the box and submit:
|
||||
<form method="post">
|
||||
<textarea name="pow_response" placeholder="script output" required></textarea>
|
||||
<div><input type="submit" value="submit" /></div>
|
||||
</form>
|
||||
</ol>
|
||||
</details>
|
||||
]]
|
||||
|
||||
-- title with favicon and hostname
|
||||
_M.site_name_section = [[
|
||||
<h3 class="pt">
|
||||
<img src="/favicon.ico" width="32" height="32" alt="icon">
|
||||
%s
|
||||
</h3>
|
||||
]]
|
||||
|
||||
-- spinner animation for proof of work
|
||||
_M.pow_section = [[
|
||||
<h3>
|
||||
Checking your browser for robots 🤖
|
||||
</h3>
|
||||
<div class="jsonly">
|
||||
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
</div>
|
||||
]]
|
||||
|
||||
-- message, captcha form and submit button
|
||||
_M.captcha_section = [[
|
||||
<h3>
|
||||
Please solve the captcha to continue.
|
||||
</h3>
|
||||
<div id="captcha" class="jsonly">
|
||||
<div class="%s" data-sitekey="%s" data-callback="onCaptchaSubmit"></div>
|
||||
<script src="%s" async defer></script>
|
||||
</div>
|
||||
]]
|
||||
|
||||
return _M
|
Reference in New Issue
Block a user