Initial commit - realms platform
This commit is contained in:
parent
c590ab6d18
commit
c717c3751c
234 changed files with 74103 additions and 15231 deletions
71
openresty/lua/fingerprint.lua
Normal file
71
openresty/lua/fingerprint.lua
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
-- Server-side fingerprint generation
|
||||
-- Generates a consistent fingerprint from IP + User-Agent + Accept-Language
|
||||
-- This cannot be spoofed by clients as it's computed server-side
|
||||
|
||||
local resty_sha256 = require "resty.sha256"
|
||||
local str = require "resty.string"
|
||||
|
||||
local _M = {}
|
||||
|
||||
-- Cache fingerprints to avoid recomputation (10 min TTL)
|
||||
local fingerprint_cache = ngx.shared.fingerprints
|
||||
|
||||
-- Generate a fingerprint from request characteristics
|
||||
-- Uses: Client IP, User-Agent, Accept-Language
|
||||
function _M.generate(custom_ip)
|
||||
-- Get client IP (respects X-Real-IP set by upstream proxy)
|
||||
local client_ip = custom_ip or ngx.var.remote_addr
|
||||
|
||||
-- Get User-Agent (empty string if not present)
|
||||
local user_agent = ngx.var.http_user_agent or ""
|
||||
|
||||
-- Get Accept-Language for additional entropy
|
||||
local accept_lang = ngx.var.http_accept_language or ""
|
||||
|
||||
-- Create a cache key from the raw inputs
|
||||
local cache_key = client_ip .. "|" .. user_agent .. "|" .. accept_lang
|
||||
|
||||
-- Check cache first
|
||||
if fingerprint_cache then
|
||||
local cached = fingerprint_cache:get(cache_key)
|
||||
if cached then
|
||||
return cached
|
||||
end
|
||||
end
|
||||
|
||||
-- Generate SHA256 hash
|
||||
local sha256 = resty_sha256:new()
|
||||
if not sha256 then
|
||||
ngx.log(ngx.ERR, "Failed to create SHA256 instance")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hash the combined data
|
||||
sha256:update(client_ip)
|
||||
sha256:update("|")
|
||||
sha256:update(user_agent)
|
||||
sha256:update("|")
|
||||
sha256:update(accept_lang)
|
||||
|
||||
local digest = sha256:final()
|
||||
local fingerprint = str.to_hex(digest)
|
||||
|
||||
-- Cache for 10 minutes (600 seconds)
|
||||
if fingerprint_cache then
|
||||
fingerprint_cache:set(cache_key, fingerprint, 600)
|
||||
end
|
||||
|
||||
return fingerprint
|
||||
end
|
||||
|
||||
-- Inject fingerprint header for proxied requests
|
||||
-- Call this in access_by_lua_block before proxy_pass
|
||||
function _M.inject_header()
|
||||
local fp = _M.generate()
|
||||
if fp then
|
||||
ngx.req.set_header("X-Server-Fingerprint", fp)
|
||||
end
|
||||
return fp
|
||||
end
|
||||
|
||||
return _M
|
||||
167
openresty/lua/jwt.lua
Normal file
167
openresty/lua/jwt.lua
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
-- JWT validation module for OpenResty edge authentication
|
||||
-- Validates JWT tokens before requests hit the backend
|
||||
local jwt = require "resty.jwt"
|
||||
|
||||
local _M = {}
|
||||
|
||||
-- Get JWT secret (cached after first call)
|
||||
local jwt_secret_cache = nil
|
||||
|
||||
local function get_jwt_secret()
|
||||
if jwt_secret_cache then
|
||||
return jwt_secret_cache
|
||||
end
|
||||
|
||||
-- os.getenv works in OpenResty when 'env JWT_SECRET;' is in nginx.conf
|
||||
local secret = os.getenv("JWT_SECRET")
|
||||
if secret and secret ~= "" then
|
||||
jwt_secret_cache = secret
|
||||
ngx.log(ngx.INFO, "JWT_SECRET loaded successfully (", string.len(secret), " chars)")
|
||||
return secret
|
||||
end
|
||||
|
||||
ngx.log(ngx.ERR, "JWT_SECRET environment variable not set or empty")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Validate JWT and inject user headers for backend
|
||||
-- Call this in access_by_lua_block for authenticated endpoints
|
||||
function _M.validate_and_inject()
|
||||
-- Get token from httpOnly cookie (name: auth_token)
|
||||
local token = ngx.var.cookie_auth_token
|
||||
if not token or token == "" then
|
||||
ngx.log(ngx.DEBUG, "No auth_token cookie found")
|
||||
ngx.status = 401
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"No authentication token"}')
|
||||
return ngx.exit(401)
|
||||
end
|
||||
|
||||
-- Get JWT secret
|
||||
local secret = get_jwt_secret()
|
||||
if not secret then
|
||||
-- SECURITY FIX: Fail closed - do NOT allow requests through if secret is missing
|
||||
-- This prevents authentication bypass if env var is misconfigured
|
||||
ngx.log(ngx.ERR, "JWT_SECRET not configured - blocking request")
|
||||
ngx.status = 500
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"Server configuration error"}')
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
-- Verify JWT signature and decode
|
||||
local jwt_obj = jwt:verify(secret, token)
|
||||
|
||||
if not jwt_obj then
|
||||
ngx.log(ngx.ERR, "JWT verification returned nil")
|
||||
ngx.status = 401
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"Invalid token"}')
|
||||
return ngx.exit(401)
|
||||
end
|
||||
|
||||
if not jwt_obj.verified then
|
||||
local reason = jwt_obj.reason or "unknown"
|
||||
ngx.log(ngx.WARN, "JWT verification failed: ", reason)
|
||||
ngx.status = 401
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"Invalid token: ' .. reason .. '"}')
|
||||
return ngx.exit(401)
|
||||
end
|
||||
|
||||
-- Payload should exist if verified
|
||||
local payload = jwt_obj.payload
|
||||
if not payload then
|
||||
ngx.log(ngx.ERR, "JWT verified but no payload")
|
||||
ngx.status = 401
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"Invalid token structure"}')
|
||||
return ngx.exit(401)
|
||||
end
|
||||
|
||||
-- Check expiry (jwt library should handle this, but be safe)
|
||||
local exp = payload.exp
|
||||
if exp then
|
||||
local now = ngx.time()
|
||||
if exp < now then
|
||||
ngx.log(ngx.DEBUG, "JWT expired: exp=", exp, " now=", now)
|
||||
ngx.status = 401
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"Token expired"}')
|
||||
return ngx.exit(401)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check issuer
|
||||
local iss = payload.iss
|
||||
if iss ~= "streaming-app" then
|
||||
ngx.log(ngx.WARN, "Invalid JWT issuer: ", tostring(iss))
|
||||
ngx.status = 401
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"Invalid token issuer"}')
|
||||
return ngx.exit(401)
|
||||
end
|
||||
|
||||
-- SECURITY: Check if user account is disabled
|
||||
local is_disabled = payload.is_disabled
|
||||
if is_disabled == "1" or is_disabled == true then
|
||||
ngx.log(ngx.WARN, "Disabled user attempted access: ", tostring(payload.username))
|
||||
ngx.status = 403
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error":"Account disabled"}')
|
||||
return ngx.exit(403)
|
||||
end
|
||||
|
||||
-- Inject user info headers for backend (allows backend to skip re-validation)
|
||||
ngx.req.set_header("X-User-ID", tostring(payload.user_id or ""))
|
||||
ngx.req.set_header("X-Username", tostring(payload.username or ""))
|
||||
ngx.req.set_header("X-Is-Admin", tostring(payload.is_admin or "0"))
|
||||
ngx.req.set_header("X-Is-Moderator", tostring(payload.is_moderator or "0"))
|
||||
ngx.req.set_header("X-Is-Streamer", tostring(payload.is_streamer or "0"))
|
||||
ngx.req.set_header("X-Is-Restreamer", tostring(payload.is_restreamer or "0"))
|
||||
ngx.req.set_header("X-Token-Version", tostring(payload.token_version or "1"))
|
||||
ngx.req.set_header("X-User-Color", tostring(payload.color_code or "#561D5E"))
|
||||
ngx.req.set_header("X-Avatar-URL", tostring(payload.avatar_url or ""))
|
||||
ngx.req.set_header("X-JWT-Validated", "true")
|
||||
|
||||
ngx.log(ngx.DEBUG, "JWT validated for user: ", tostring(payload.username))
|
||||
end
|
||||
|
||||
-- Optional: Get user info without blocking (for endpoints that work with or without auth)
|
||||
-- Returns user payload if authenticated, nil otherwise
|
||||
function _M.get_user_if_authenticated()
|
||||
local token = ngx.var.cookie_auth_token
|
||||
if not token or token == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local secret = get_jwt_secret()
|
||||
if not secret then
|
||||
return nil
|
||||
end
|
||||
|
||||
local jwt_obj = jwt:verify(secret, token)
|
||||
if not jwt_obj or not jwt_obj.verified then
|
||||
return nil
|
||||
end
|
||||
|
||||
local payload = jwt_obj.payload
|
||||
if not payload then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Check expiry
|
||||
if payload.exp and payload.exp < ngx.time() then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Check disabled
|
||||
local is_disabled = payload.is_disabled
|
||||
if is_disabled == "1" or is_disabled == true then
|
||||
return nil
|
||||
end
|
||||
|
||||
return payload
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
@ -2,19 +2,33 @@ local redis = require "resty.redis"
|
|||
|
||||
local _M = {}
|
||||
|
||||
-- Cache password at module load time (works better with OpenResty)
|
||||
local REDIS_PASSWORD = os.getenv("REDIS_PASS")
|
||||
|
||||
local function get_redis_connection()
|
||||
local red = redis:new()
|
||||
red:set_timeouts(1000, 1000, 1000) -- connect, send, read timeout in ms
|
||||
|
||||
|
||||
local host = "redis" -- Will be resolved by nginx resolver
|
||||
local port = tonumber(os.getenv("REDIS_PORT")) or 6379
|
||||
|
||||
|
||||
local ok, err = red:connect(host, port)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
-- Authenticate if password is set
|
||||
if REDIS_PASSWORD and REDIS_PASSWORD ~= "" then
|
||||
local res, err = red:auth(REDIS_PASSWORD)
|
||||
if not res then
|
||||
ngx.log(ngx.ERR, "Failed to authenticate to Redis: ", err)
|
||||
return nil
|
||||
end
|
||||
else
|
||||
ngx.log(ngx.WARN, "No Redis password set, trying without auth")
|
||||
end
|
||||
|
||||
return red
|
||||
end
|
||||
|
||||
|
|
@ -69,7 +83,8 @@ function _M.validate_viewer_token(token, expected_stream_key)
|
|||
|
||||
-- Check if the token is for the expected stream
|
||||
if res ~= expected_stream_key then
|
||||
ngx.log(ngx.WARN, "Token stream mismatch. Expected: ", expected_stream_key, " Got: ", res)
|
||||
-- SECURITY FIX: Redact stream keys from logs to prevent exposure
|
||||
ngx.log(ngx.WARN, "Token stream mismatch. Expected hash: ", ngx.md5(expected_stream_key):sub(1, 8), " Got hash: ", ngx.md5(res):sub(1, 8))
|
||||
return false
|
||||
end
|
||||
|
||||
|
|
@ -81,9 +96,9 @@ function _M.refresh_viewer_token(token)
|
|||
if not red then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Refresh TTL to 30 seconds
|
||||
local ok, err = red:expire("viewer_token:" .. token, 30)
|
||||
|
||||
-- Refresh TTL to 5 minutes on each HLS segment access
|
||||
local ok, err = red:expire("viewer_token:" .. token, 300)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "Failed to refresh token TTL: ", err)
|
||||
end
|
||||
|
|
|
|||
180
openresty/lua/thumbnail.lua
Normal file
180
openresty/lua/thumbnail.lua
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
local _M = {}
|
||||
|
||||
local THUMB_DIR = "/tmp/thumbs"
|
||||
local CACHE_TTL = 5 -- seconds before regenerating
|
||||
local FFMPEG_TIMEOUT = 15 -- seconds (needs more time for animated capture)
|
||||
local ANIMATION_DURATION = 3 -- seconds of video to capture
|
||||
local ANIMATION_FPS = 8 -- frames per second in output
|
||||
|
||||
-- SECURITY FIX: Shell escape function to prevent command injection
|
||||
local function shell_escape(str)
|
||||
-- Escape single quotes by ending the quoted string, adding escaped quote, and starting new quoted string
|
||||
return "'" .. string.gsub(str, "'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
local function file_exists(path)
|
||||
local f = io.open(path, "r")
|
||||
if f then
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function file_size(path)
|
||||
local f = io.open(path, "rb")
|
||||
if f then
|
||||
local size = f:seek("end")
|
||||
f:close()
|
||||
return size or 0
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function file_age(path)
|
||||
-- Get file modification time using stat
|
||||
local handle = io.popen("stat -c %Y " .. path .. " 2>/dev/null")
|
||||
if not handle then
|
||||
return nil
|
||||
end
|
||||
local mtime = handle:read("*a")
|
||||
handle:close()
|
||||
|
||||
if not mtime or mtime == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local now = os.time()
|
||||
return now - tonumber(mtime)
|
||||
end
|
||||
|
||||
local function generate_thumbnail(stream_key, thumb_path)
|
||||
-- Build LLHLS URL - internal docker network
|
||||
local llhls_url = "http://ovenmediaengine:8080/app/" .. stream_key .. "/llhls.m3u8"
|
||||
local log_file = THUMB_DIR .. "/" .. stream_key .. ".log"
|
||||
|
||||
-- First, try to generate animated WebP
|
||||
-- Using os.execute which blocks properly until complete
|
||||
-- SECURITY FIX: Use shell_escape to prevent command injection
|
||||
local cmd = string.format(
|
||||
"timeout %d ffmpeg -y -i %s -t %d -vf 'fps=%d,scale=320:-1:flags=lanczos' -c:v libwebp -lossless 0 -compression_level 3 -q:v 70 -loop 0 -preset default -an %s > %s 2>&1",
|
||||
FFMPEG_TIMEOUT,
|
||||
shell_escape(llhls_url),
|
||||
ANIMATION_DURATION,
|
||||
ANIMATION_FPS,
|
||||
shell_escape(thumb_path),
|
||||
shell_escape(log_file)
|
||||
)
|
||||
|
||||
ngx.log(ngx.INFO, "Generating thumbnail for stream: ", stream_key)
|
||||
|
||||
local exit_code = os.execute(cmd)
|
||||
|
||||
-- Check if file was created and has content
|
||||
if file_exists(thumb_path) and file_size(thumb_path) > 100 then
|
||||
ngx.log(ngx.INFO, "Thumbnail generated successfully: ", thumb_path)
|
||||
os.remove(log_file)
|
||||
return true
|
||||
end
|
||||
|
||||
-- If animated webp failed, try static webp (single frame)
|
||||
ngx.log(ngx.WARN, "Animated webp failed (exit: ", tostring(exit_code), "), trying static")
|
||||
|
||||
-- SECURITY FIX: Use shell_escape to prevent command injection
|
||||
local static_cmd = string.format(
|
||||
"timeout %d ffmpeg -y -i %s -vframes 1 -vf 'scale=320:-1:flags=lanczos' -c:v libwebp -q:v 75 %s > %s 2>&1",
|
||||
FFMPEG_TIMEOUT,
|
||||
shell_escape(llhls_url),
|
||||
shell_escape(thumb_path),
|
||||
shell_escape(log_file)
|
||||
)
|
||||
|
||||
exit_code = os.execute(static_cmd)
|
||||
|
||||
if file_exists(thumb_path) and file_size(thumb_path) > 100 then
|
||||
ngx.log(ngx.INFO, "Static thumbnail generated: ", thumb_path)
|
||||
os.remove(log_file)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Read log file for error info
|
||||
local log_content = ""
|
||||
local log_f = io.open(log_file, "r")
|
||||
if log_f then
|
||||
log_content = log_f:read("*a") or ""
|
||||
log_f:close()
|
||||
end
|
||||
|
||||
ngx.log(ngx.ERR, "Thumbnail generation failed (exit: ", tostring(exit_code), "): ", log_content:sub(-500))
|
||||
return false
|
||||
end
|
||||
|
||||
function _M.serve()
|
||||
-- Get stream key from nginx variable
|
||||
local stream_key = ngx.var.stream_key
|
||||
if not stream_key or stream_key == "" then
|
||||
ngx.status = 400
|
||||
ngx.header["Content-Type"] = "text/plain"
|
||||
ngx.say("Missing stream key")
|
||||
return ngx.exit(400)
|
||||
end
|
||||
|
||||
-- Sanitize stream key (alphanumeric, dash, underscore only)
|
||||
if not stream_key:match("^[%w%-_]+$") then
|
||||
ngx.status = 400
|
||||
ngx.header["Content-Type"] = "text/plain"
|
||||
ngx.say("Invalid stream key")
|
||||
return ngx.exit(400)
|
||||
end
|
||||
|
||||
local thumb_path = THUMB_DIR .. "/" .. stream_key .. ".webp"
|
||||
|
||||
-- Check if cached thumbnail exists and is fresh
|
||||
local age = file_age(thumb_path)
|
||||
local needs_refresh = not age or age > CACHE_TTL
|
||||
|
||||
if needs_refresh then
|
||||
-- Generate new thumbnail
|
||||
local ok = generate_thumbnail(stream_key, thumb_path)
|
||||
if not ok then
|
||||
-- If generation failed, check if we have a stale one to serve
|
||||
if not file_exists(thumb_path) or file_size(thumb_path) < 100 then
|
||||
ngx.status = 503
|
||||
ngx.header["Content-Type"] = "text/plain"
|
||||
ngx.header["Retry-After"] = "5"
|
||||
ngx.say("Thumbnail generation in progress")
|
||||
return ngx.exit(503)
|
||||
end
|
||||
-- Serve stale thumbnail
|
||||
end
|
||||
end
|
||||
|
||||
-- Verify file exists and has content
|
||||
if not file_exists(thumb_path) or file_size(thumb_path) < 100 then
|
||||
ngx.status = 503
|
||||
ngx.header["Content-Type"] = "text/plain"
|
||||
ngx.header["Retry-After"] = "5"
|
||||
ngx.say("Thumbnail not ready")
|
||||
return ngx.exit(503)
|
||||
end
|
||||
|
||||
-- Serve the thumbnail file
|
||||
ngx.header["Content-Type"] = "image/webp"
|
||||
ngx.header["Cache-Control"] = "public, max-age=" .. CACHE_TTL
|
||||
ngx.header["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
local f = io.open(thumb_path, "rb")
|
||||
if not f then
|
||||
ngx.status = 500
|
||||
ngx.header["Content-Type"] = "text/plain"
|
||||
ngx.say("Failed to read thumbnail")
|
||||
return ngx.exit(500)
|
||||
end
|
||||
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
|
||||
ngx.print(content)
|
||||
end
|
||||
|
||||
return _M
|
||||
118
openresty/lua/uberban.lua
Normal file
118
openresty/lua/uberban.lua
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
-- Site-wide uberban checking module
|
||||
-- Blocks uberbanned fingerprints from accessing any endpoint
|
||||
|
||||
local redis = require "resty.redis"
|
||||
local fingerprint = require "fingerprint"
|
||||
|
||||
local _M = {}
|
||||
|
||||
-- Cache for ban status (5 second TTL to reduce Redis load)
|
||||
local uberban_cache = ngx.shared.uberban_cache
|
||||
local CACHE_TTL = 5 -- seconds
|
||||
|
||||
-- Redis password cached at module load time
|
||||
local REDIS_PASSWORD = os.getenv("REDIS_PASS")
|
||||
|
||||
-- Get Redis connection (same pattern as redis_helper.lua)
|
||||
local function get_redis_connection()
|
||||
local red = redis:new()
|
||||
red:set_timeouts(1000, 1000, 1000) -- connect, send, read timeout in ms
|
||||
|
||||
local host = "redis"
|
||||
local port = tonumber(os.getenv("REDIS_PORT")) or 6379
|
||||
|
||||
local ok, err = red:connect(host, port)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "[uberban] Failed to connect to Redis: ", err)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Authenticate if password is set
|
||||
if REDIS_PASSWORD and REDIS_PASSWORD ~= "" then
|
||||
local res, err = red:auth(REDIS_PASSWORD)
|
||||
if not res then
|
||||
ngx.log(ngx.ERR, "[uberban] Failed to authenticate to Redis: ", err)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Select database 1 (where chat-service stores fingerprint bans)
|
||||
local db = tonumber(os.getenv("REDIS_DB")) or 1
|
||||
local res, err = red:select(db)
|
||||
if not res then
|
||||
ngx.log(ngx.ERR, "[uberban] Failed to select Redis db ", db, ": ", err)
|
||||
return nil
|
||||
end
|
||||
|
||||
return red
|
||||
end
|
||||
|
||||
local function close_redis_connection(red)
|
||||
local ok, err = red:set_keepalive(10000, 100)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "[uberban] Failed to set keepalive: ", err)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if a fingerprint is uberbanned
|
||||
-- Returns true if banned, false if not banned
|
||||
function _M.is_banned(fp)
|
||||
if not fp then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check cache first
|
||||
if uberban_cache then
|
||||
local cached = uberban_cache:get(fp)
|
||||
if cached ~= nil then
|
||||
return cached == "1"
|
||||
end
|
||||
end
|
||||
|
||||
-- Query Redis
|
||||
local red = get_redis_connection()
|
||||
if not red then
|
||||
-- SECURITY FIX: Fail closed - if Redis is unavailable, deny access
|
||||
-- This prevents banned users from accessing the system during outages
|
||||
ngx.log(ngx.ERR, "[uberban] Redis unavailable - blocking request (fail closed)")
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if fingerprint is in the uberbanned set
|
||||
local res, err = red:sismember("chat:banned:fingerprints", fp)
|
||||
close_redis_connection(red)
|
||||
|
||||
if not res then
|
||||
ngx.log(ngx.ERR, "[uberban] Redis SISMEMBER failed: ", err)
|
||||
return false
|
||||
end
|
||||
|
||||
local is_banned = (res == 1)
|
||||
|
||||
-- Cache the result
|
||||
if uberban_cache then
|
||||
uberban_cache:set(fp, is_banned and "1" or "0", CACHE_TTL)
|
||||
end
|
||||
|
||||
return is_banned
|
||||
end
|
||||
|
||||
-- Check fingerprint and block if uberbanned
|
||||
-- Call this in access_by_lua_block
|
||||
function _M.check_and_block()
|
||||
-- Generate server-side fingerprint
|
||||
local fp = fingerprint.generate()
|
||||
if not fp then
|
||||
return -- Can't generate fingerprint, allow request
|
||||
end
|
||||
|
||||
-- Check if banned
|
||||
if _M.is_banned(fp) then
|
||||
ngx.status = ngx.HTTP_FORBIDDEN
|
||||
ngx.header["Content-Type"] = "application/json"
|
||||
ngx.say('{"error": "Access denied", "banned": true}')
|
||||
return ngx.exit(ngx.HTTP_FORBIDDEN)
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
||||
Loading…
Add table
Add a link
Reference in a new issue