Compare commits

...

10 commits

Author SHA1 Message Date
d8aece9d70 use csprng properly, dont need full random 2025-11-23 17:55:37 -03:00
89dc5da94e unify stylesheets 2025-11-23 17:53:26 -03:00
0b0a9c7aaa ddos: use csprng for tokens 2025-11-23 17:48:57 -03:00
2cbf796aef make pow test fail much earlier
(should never fail)
2025-11-23 17:33:27 -03:00
9dd95c82a7 move more things to test ngx 2025-11-23 17:26:54 -03:00
37e9af217f use metatable so ngx.shared is created dynamically 2025-11-23 17:23:21 -03:00
4795879278 configure shared maps globally 2025-11-23 17:23:00 -03:00
be059b26c1 make tests actually finish challenges quickly 2025-11-23 17:19:07 -03:00
Claude
fbe238d3c1 Add mock resty.sha256 and resty.string for testing
The test environment doesn't have OpenResty libraries, so we need
to provide mock implementations for testing.

Created:
- tests/mock_resty_sha256.lua: Uses system sha256sum command to
  compute SHA-256 hashes. Mimics the resty.sha256 API (new,
  update, final).

- tests/mock_resty_string.lua: Implements to_hex() to convert
  binary strings to hexadecimal.

Updated test.lua to preload these mocks so that when the module
or tests require 'resty.sha256' or 'resty.string', they get our
mock implementations instead.

This allows the PoW verification tests to run and actually verify
the SHA-256 proof-of-work.
2025-11-23 17:19:07 -03:00
Claude
60c6c10b0f SECURITY: Implement server-side SHA-256 verification for PoW
Major security fix: The proof-of-work challenge was previously
just trusting the client, allowing bots to bypass it by submitting
random nonces without doing any work.

Changes:
- Added proper server-side SHA-256 verification using resty.sha256
- Server now verifies that sha256(challenge + nonce) has the
  required number of leading zeros based on pow_difficulty
- Bots must now actually compute the proof-of-work

Updated tests:
- Added computeValidNonce() helper that actually computes valid
  nonces by brute force (for testing purposes)
- testValidPowPassesChallenge now uses a real computed nonce

Updated README to explicitly mention server-side verification.
2025-11-23 17:19:07 -03:00
8 changed files with 320 additions and 391 deletions

View file

@ -10,8 +10,10 @@ aproxy is an Activity Pub Reverse Proxy Framework built on OpenResty (NGINX + Lu
### Testing
```sh
# Install test dependencies (only needed once)
# Install test dependencies (only needed once for project setup)
make testdeps
# run this to setup the PATH so that it works
eval (luarocks-5.1 path --bin)
# Run test suite

View file

@ -207,6 +207,8 @@ pow_difficulty = 4 -- Difficulty levels:
This creates real computational cost for attackers - a bot making 1000 requests/sec would need to spend 1000-3000 seconds of CPU time with difficulty 4.
**Security**: The server verifies the proof-of-work by computing `SHA-256(challenge + nonce)` and checking that it has the required leading zeros. Bots cannot bypass this by submitting random nonces.
## Path-Based Protection
You can configure the module to protect only specific paths, which is useful for:
@ -250,23 +252,8 @@ protected_paths = {}
4. **Use PCRE regex**: Patterns are PCRE regular expressions, so you can use advanced patterns like `^/api/v[0-9]+/search$` for complex matching.
## Testing
The module includes comprehensive tests. To run them:
```bash
# Install test dependencies (once)
make testdeps
eval $(luarocks-5.1 path --bin)
# Run tests
make test
```
## Security Considerations
1. **Token Generation**: The module uses Lua's `math.random` for token generation. For production use with high security requirements, consider integrating a cryptographically secure random source.
2. **Cookie Security**: Cookies are set with `HttpOnly` and `SameSite=Lax` flags for security. Consider adding `Secure` flag if you're running HTTPS only.
3. **Shared Dictionary Size**: Size the shared dictionaries appropriately:
@ -279,37 +266,3 @@ make test
set_real_ip_from 10.0.0.0/8; # Your proxy IP range
real_ip_header X-Forwarded-For;
```
## Limitations
- **Shared State**: Uses nginx shared memory, so it's per-server. In a multi-server setup, bans and tokens aren't shared across servers.
- **Memory Limits**: Shared dictionaries have fixed sizes. Old entries are evicted when full (LRU).
- **Not a Complete Solution**: This helps with volumetric attacks and simple bots, but sophisticated attackers can bypass it. Use in combination with other security measures.
## Combining With Other Scripts
This module can be combined with other aproxy scripts for defense in depth:
```lua
return {
version = 1,
wantedScripts = {
-- First layer: Challenge-response for DDoS/bot protection
['ddos_protection_challenge'] = {
ban_duration = 3600,
token_duration = 86400,
-- ... config
},
-- Second layer: Restrict specific expensive endpoints
['pleroma_restrict_unauthenticated_search'] = {},
-- Third layer: Allowlist for webfinger
['webfinger_allowlist'] = {
accounts = {'user@domain.com'}
}
}
}
```
Scripts execute in order, so the challenge runs first, filtering out bots before they hit your more specific rules.

View file

@ -2,18 +2,13 @@
-- Similar to Cloudflare's "Under Attack" mode
-- Presents a challenge page with a honeypot link
local resty_random = require "resty.random"
local resty_string = require "resty.string"
local function generateToken()
-- Generate a random token for validation
-- Using ngx.time() and math.random for simplicity
-- In production, consider using a more secure method
math.randomseed(ngx.time() * 1000 + math.random(1, 1000))
local chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
local token = {}
for i = 1, 32 do
local idx = math.random(1, #chars)
token[i] = chars:sub(idx, idx)
end
return table.concat(token)
-- Generate a cryptographically strong random token
-- Uses RAND_pseudo_bytes which is secure and won't fail
return resty_string.to_hex(resty_random.bytes(16))
end
local function getCookieValue(cookie_header, cookie_name)
@ -27,6 +22,79 @@ local function getCookieValue(cookie_header, cookie_name)
return value
end
-- Common CSS styles for all challenge pages
local COMMON_STYLES = [[
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 3rem;
border-radius: 1rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 450px;
text-align: center;
}
h1 {
color: #333;
margin-bottom: 1rem;
font-size: 1.75rem;
}
p {
color: #666;
margin-bottom: 2rem;
line-height: 1.6;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 1rem 2rem;
font-size: 1rem;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
.honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.spinner {
display: none;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 1rem auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading button { display: none; }
.loading .spinner { display: block; }
]]
-- Question pool for multiple-choice challenges
local QUESTIONS = {
{q = "What is 7 + 5?", answers = {"10", "12", "14", "15"}, correct = 2},
@ -60,7 +128,6 @@ local function challengeInit(cfg)
end
local function serveButtonChallenge(original_uri)
-- Simple button challenge page with honeypot
local html = [[
<!DOCTYPE html>
<html>
@ -68,78 +135,7 @@ local function serveButtonChallenge(original_uri)
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Check</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 3rem;
border-radius: 1rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 400px;
text-align: center;
}
h1 {
color: #333;
margin-bottom: 1rem;
font-size: 1.75rem;
}
p {
color: #666;
margin-bottom: 2rem;
line-height: 1.6;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 1rem 2rem;
font-size: 1rem;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
/* Honeypot link - invisible to humans but crawlable by bots */
.honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.spinner {
display: none;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 1rem auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading button { display: none; }
.loading .spinner { display: block; }
</style>
<style>]] .. COMMON_STYLES .. [[</style>
</head>
<body>
<div class="container" id="container">
@ -150,7 +146,6 @@ local function serveButtonChallenge(original_uri)
<button type="submit">Verify I'm Human</button>
</form>
<div class="spinner"></div>
<!-- Honeypot link for bots -->
<a href="/__aproxy_challenge_trap" class="honeypot">Click here to continue</a>
</div>
</body>
@ -161,8 +156,8 @@ end
local function serveQuestionChallenge(original_uri, state)
-- Select a random question
math.randomseed(ngx.time() * 1000 + math.random(1, 1000))
local q_idx = math.random(1, #QUESTIONS)
local random_byte = resty_random.bytes(1)
local q_idx = (string.byte(random_byte) % #QUESTIONS) + 1
local question = QUESTIONS[q_idx]
-- Generate a challenge ID to store the correct answer
@ -188,77 +183,33 @@ local function serveQuestionChallenge(original_uri, state)
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Check</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 3rem;
border-radius: 1rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 450px;
text-align: center;
}
h1 {
color: #333;
margin-bottom: 1rem;
font-size: 1.75rem;
}
.question {
color: #444;
font-size: 1.1rem;
margin: 2rem 0;
font-weight: 500;
}
.options {
text-align: left;
margin: 2rem 0;
}
.option {
display: block;
padding: 1rem;
margin: 0.75rem 0;
border: 2px solid #e0e0e0;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.option:hover {
border-color: #667eea;
background: #f8f9ff;
}
.option input[type="radio"] {
margin-right: 0.75rem;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 1rem 2rem;
font-size: 1rem;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
]] .. COMMON_STYLES .. [[
.question {
color: #444;
font-size: 1.1rem;
margin: 2rem 0;
font-weight: 500;
}
.options {
text-align: left;
margin: 2rem 0;
}
.option {
display: block;
padding: 1rem;
margin: 0.75rem 0;
border: 2px solid #e0e0e0;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.option:hover {
border-color: #667eea;
background: #f8f9ff;
}
.option input[type="radio"] {
margin-right: 0.75rem;
}
</style>
</head>
<body>
@ -296,59 +247,19 @@ local function servePowChallenge(original_uri, state)
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Security Check</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 3rem;
border-radius: 1rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 450px;
text-align: center;
}
h1 {
color: #333;
margin-bottom: 1rem;
font-size: 1.75rem;
}
p {
color: #666;
margin-bottom: 1rem;
line-height: 1.6;
}
.status {
font-family: monospace;
color: #667eea;
font-weight: 600;
margin: 1rem 0;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 1.5rem auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
]] .. COMMON_STYLES .. [[
.status {
font-family: monospace;
color: #667eea;
font-weight: 600;
margin: 1rem 0;
}
.spinner {
display: block;
width: 40px;
height: 40px;
margin: 1.5rem auto;
}
</style>
</head>
<body>
@ -468,14 +379,24 @@ local function challengeCallback(cfg, state)
local challenge_exists = state.tokens_dict:get("pow:" .. challenge)
if challenge_exists then
-- Verify the hash using resty.sha256 or just trust client for now
-- In production, you'd want to verify: sha256(challenge + nonce) starts with N zeros
-- For simplicity, we'll trust the client solved it correctly
-- A more secure implementation would verify server-side
challenge_passed = true
-- Verify the proof-of-work server-side
local resty_sha256 = require("resty.sha256")
local str = require("resty.string")
-- Clean up the challenge
state.tokens_dict:delete("pow:" .. challenge)
local sha256 = resty_sha256:new()
sha256:update(challenge .. nonce)
local digest = sha256:final()
local hash_hex = str.to_hex(digest)
-- Check if hash starts with required number of zeros
local required_zeros = string.rep("0", state.pow_difficulty)
if hash_hex:sub(1, state.pow_difficulty) == required_zeros then
challenge_passed = true
-- Clean up the challenge
state.tokens_dict:delete("pow:" .. challenge)
else
ngx.log(ngx.WARN, "PoW verification failed: hash doesn't have enough leading zeros")
end
end
end

View file

@ -2,6 +2,56 @@ lu = require('luaunit')
local rex = require('rex_pcre2')
require('util')
-- Preload mock resty libraries for testing
package.preload['resty.sha256'] = function()
return require('tests.mock_resty_sha256')
end
package.preload['resty.string'] = function()
return require('tests.mock_resty_string')
end
package.preload['resty.random'] = function()
return require('tests.mock_resty_random')
end
-- Create a mock shared dictionary
local function createMockSharedDict()
local storage = {}
return {
get = function(self, key)
local item = storage[key]
if not item then return nil end
if item.expiry and item.expiry < ngx.time() then
storage[key] = nil
return nil
end
return item.value
end,
set = function(self, key, value, exptime)
storage[key] = {
value = value,
expiry = exptime and (ngx.time() + exptime) or nil
}
return true, nil
end,
delete = function(self, key)
storage[key] = nil
end
}
end
-- Create a proxy table that dynamically creates mock dictionaries on access
local function createSharedProxy()
local dicts = {}
return setmetatable({}, {
__index = function(_, key)
if not dicts[key] then
dicts[key] = createMockSharedDict()
end
return dicts[key]
end
})
end
function createNgx()
local ngx = {
status = nil
@ -27,7 +77,9 @@ function createNgx()
ngx.ERR = 3
-- only hold data here
ngx.var = {}
ngx.var = {
request_method = 'GET'
}
-- request params api
ngx.req = {}
@ -40,11 +92,30 @@ function createNgx()
ngx._uri_args = val
end
ngx.req.read_body = function() end
ngx.req.get_post_args = function()
return ngx._post_args or {}
end
ngx.req.get_headers = function()
return ngx._headers or {}
end
-- response headers
ngx.header = {}
-- regex api
ngx.re = {}
ngx.re.match = rex.match
ngx.re.search = rex.find
-- shared memory dictionaries for testing (dynamically created on access)
ngx.shared = createSharedProxy()
-- time function for testing
ngx.time = function() return 1000000 end
return ngx
end

View file

@ -1,55 +1,9 @@
TestDDoSProtectionChallenge = {}
-- Mock shared dictionary
local function createMockSharedDict()
local storage = {}
return {
get = function(self, key)
local item = storage[key]
if not item then return nil end
if item.expiry and item.expiry < ngx.time() then
storage[key] = nil
return nil
end
return item.value
end,
set = function(self, key, value, exptime)
storage[key] = {
value = value,
expiry = exptime and (ngx.time() + exptime) or nil
}
return true, nil
end,
delete = function(self, key)
storage[key] = nil
end
}
end
function TestDDoSProtectionChallenge:setup()
-- Reset ngx
-- Reset ngx (includes shared dicts, time, and request functions)
resetNgx()
-- Create mock shared dictionaries AFTER resetNgx
ngx.shared = {
aproxy_bans = createMockSharedDict(),
aproxy_tokens = createMockSharedDict()
}
-- Mock ngx.time for consistent testing
ngx.time = function() return 1000000 end
-- Add missing ngx.req functions
ngx.req.read_body = function() end
ngx.req.get_post_args = function()
return ngx._post_args or {}
end
ngx.req.get_headers = function()
return ngx._headers or {}
end
ngx.header = {}
ngx.var.request_method = 'GET'
-- Setup module manually (can't use setupTest because it calls resetNgx)
local ctx = require('ctx')
local config = require('config')
@ -205,9 +159,6 @@ function TestDDoSProtectionChallenge:testExpiredTokenShowsChallenge()
end
function TestDDoSProtectionChallenge:teardown()
ngx.shared = nil
ngx.header = nil
ngx._post_args = nil
teardownNgx()
end
@ -215,28 +166,9 @@ end
TestDDoSProtectionChallengePaths = {}
function TestDDoSProtectionChallengePaths:setup()
-- Reset ngx
-- Reset ngx (includes shared dicts, time, and request functions)
resetNgx()
-- Create mock shared dictionaries AFTER resetNgx
ngx.shared = {
aproxy_bans = createMockSharedDict(),
aproxy_tokens = createMockSharedDict()
}
ngx.time = function() return 1000000 end
-- Add missing ngx.req functions
ngx.req.read_body = function() end
ngx.req.get_post_args = function()
return ngx._post_args or {}
end
ngx.req.get_headers = function()
return ngx._headers or {}
end
ngx.header = {}
ngx.var.request_method = 'GET'
-- Setup module manually
local ctx = require('ctx')
local config = require('config')
@ -372,9 +304,6 @@ function TestDDoSProtectionChallengePaths:testBannedIPBlockedOnUnprotectedPath()
end
function TestDDoSProtectionChallengePaths:teardown()
ngx.shared = nil
ngx.header = nil
ngx._post_args = nil
teardownNgx()
end
@ -382,28 +311,9 @@ end
TestDDoSProtectionChallengeQuestion = {}
function TestDDoSProtectionChallengeQuestion:setup()
-- Reset ngx
-- Reset ngx (includes shared dicts, time, and request functions)
resetNgx()
-- Create mock shared dictionaries AFTER resetNgx
ngx.shared = {
aproxy_bans = createMockSharedDict(),
aproxy_tokens = createMockSharedDict()
}
ngx.time = function() return 1000000 end
-- Add missing ngx.req functions
ngx.req.read_body = function() end
ngx.req.get_post_args = function()
return ngx._post_args or {}
end
ngx.req.get_headers = function()
return ngx._headers or {}
end
ngx.header = {}
ngx.var.request_method = 'GET'
-- Setup module manually
local ctx = require('ctx')
local config = require('config')
@ -487,38 +397,36 @@ function TestDDoSProtectionChallengeQuestion:testWrongAnswerFailsChallenge()
end
function TestDDoSProtectionChallengeQuestion:teardown()
ngx.shared = nil
ngx.header = nil
ngx._post_args = nil
teardownNgx()
end
-- Helper function to compute a valid PoW nonce for testing
local function computeValidNonce(challenge, difficulty)
local resty_sha256 = require("resty.sha256")
local str = require("resty.string")
local required_zeros = string.rep("0", difficulty)
for nonce = 0, 150 do
local sha256 = resty_sha256:new()
sha256:update(challenge .. tostring(nonce))
local digest = sha256:final()
local hash_hex = str.to_hex(digest)
if hash_hex:sub(1, difficulty) == required_zeros then
return tostring(nonce)
end
end
error("Could not find valid nonce after 150 attempts")
end
-- Tests for proof-of-work challenge type
TestDDoSProtectionChallengePow = {}
function TestDDoSProtectionChallengePow:setup()
-- Reset ngx
-- Reset ngx (includes shared dicts, time, and request functions)
resetNgx()
-- Create mock shared dictionaries AFTER resetNgx
ngx.shared = {
aproxy_bans = createMockSharedDict(),
aproxy_tokens = createMockSharedDict()
}
ngx.time = function() return 1000000 end
-- Add missing ngx.req functions
ngx.req.read_body = function() end
ngx.req.get_post_args = function()
return ngx._post_args or {}
end
ngx.req.get_headers = function()
return ngx._headers or {}
end
ngx.header = {}
ngx.var.request_method = 'GET'
-- Setup module manually
local ctx = require('ctx')
local config = require('config')
@ -530,7 +438,7 @@ function TestDDoSProtectionChallengePow:setup()
shared_dict_tokens = 'aproxy_tokens',
protected_paths = {},
challenge_type = 'pow',
pow_difficulty = 4
pow_difficulty = 1
}
self.mod = require('scripts.ddos_protection_challenge')
@ -558,17 +466,30 @@ function TestDDoSProtectionChallengePow:testPowChallengeShown()
lu.assertStrContains(ngx._say, '<script>')
end
local function generateToken()
local chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
local token = {}
for i = 1, 32 do
local idx = math.random(1, #chars)
token[i] = chars:sub(idx, idx)
end
return table.concat(token)
end
function TestDDoSProtectionChallengePow:testValidPowPassesChallenge()
local test_challenge = 'test_pow_challenge'
local test_challenge = generateToken()
ngx.shared.aproxy_tokens:set('pow:' .. test_challenge, true, 300)
-- Compute a valid nonce that satisfies the PoW requirement
local valid_nonce = computeValidNonce(test_challenge, 1)
setupFakeRequest('/__aproxy_challenge_verify', {})
ngx.var.remote_addr = '192.168.4.2'
ngx.var.request_method = 'POST'
ngx._post_args = {
return_to = '/api/test',
challenge = test_challenge,
nonce = '12345' -- In reality, this would be computed
nonce = valid_nonce
}
onRequest()
@ -596,8 +517,5 @@ function TestDDoSProtectionChallengePow:testInvalidPowFailsChallenge()
end
function TestDDoSProtectionChallengePow:teardown()
ngx.shared = nil
ngx.header = nil
ngx._post_args = nil
teardownNgx()
end

View file

@ -0,0 +1,16 @@
-- Mock implementation of resty.random for testing
local random = {}
function random.bytes(len, strong)
-- For testing, generate predictable random bytes using math.random
-- In real OpenResty, this would use cryptographic random sources
local bytes = {}
for i = 1, len do
-- Use math.random to generate bytes (0-255)
table.insert(bytes, string.char(math.random(0, 255)))
end
return table.concat(bytes)
end
return random

View file

@ -0,0 +1,35 @@
-- Mock implementation of resty.sha256 for testing
-- Uses system sha256sum command since we don't have OpenResty libraries
local sha256 = {}
sha256.__index = sha256
function sha256:new()
local obj = {
buffer = ""
}
setmetatable(obj, sha256)
return obj
end
function sha256:update(data)
self.buffer = self.buffer .. data
end
function sha256:final()
-- Use sha256sum command to compute hash
local handle = io.popen("echo -n '" .. self.buffer:gsub("'", "'\\''") .. "' | sha256sum")
local result = handle:read("*a")
handle:close()
-- Parse hex string into binary
local hex = result:match("^(%x+)")
local binary = {}
for i = 1, #hex, 2 do
table.insert(binary, string.char(tonumber(hex:sub(i, i+1), 16)))
end
return table.concat(binary)
end
return sha256

View file

@ -0,0 +1,13 @@
-- Mock implementation of resty.string for testing
local str = {}
function str.to_hex(binary)
local hex = {}
for i = 1, #binary do
table.insert(hex, string.format("%02x", string.byte(binary, i)))
end
return table.concat(hex)
end
return str