return to default

This commit is contained in:
wiaamm
2025-12-06 13:19:51 +02:00
parent b187507406
commit 6924be03b2
2 changed files with 110 additions and 345 deletions

View File

@@ -15,42 +15,20 @@ function NanoHandler.init_worker()
nano.init_attachment()
end
-- **Handles Request Headers (DecodeHeaders Equivalent)**
function NanoHandler.access(conf)
kong.log.debug("1-ACCESS PHASE START ========================================")
if not kong.router.get_route() then
kong.log.debug("ACCESS SKIPPED: no route matched")
return
end
local request_path = kong.request.get_path()
if request_path and (
request_path:match("^/status") or
request_path:match("^/_health") or
request_path:match("^/metrics")
) then
kong.log.debug("ACCESS SKIPPED: internal endpoint: ", request_path)
return
end
if ngx.var.internal then
kong.log.debug("ACCESS SKIPPED: internal subrequest")
return
end
local request_uri = ngx.var.request_uri
if not request_uri or request_uri == "" then
kong.log.debug("ACCESS SKIPPED: TLS handshake or no URI")
return
end
local headers = kong.request.get_headers()
local session_id = nano.generate_session_id()
kong.service.request.set_header("x-session-id", tostring(session_id))
if NanoHandler.processed_requests[session_id] then
kong.ctx.plugin.blocked = true
return
end
local session_data = nano.init_session(session_id)
if not session_data then
kong.log.err("Failed to initialize session - failing open (no session created)")
kong.log.err("Failed to initialize session - failing open")
return
end
@@ -59,11 +37,7 @@ function NanoHandler.access(conf)
local meta_data = nano.handle_start_transaction()
if not meta_data then
kong.log.err("Failed to handle start transaction - cleaning up session and failing open")
nano.fini_session(session_data)
nano.cleanup_all()
kong.ctx.plugin.session_id = nil
kong.ctx.plugin.session_data = nil
kong.log.err("Failed to handle start transaction - failing open")
return
end
@@ -73,15 +47,11 @@ function NanoHandler.access(conf)
local contains_body = has_content_length and 1 or 0
local verdict, response = nano.send_data(session_id, session_data, meta_data, req_headers, contains_body, nano.HttpChunkType.HTTP_REQUEST_FILTER)
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in access/send_data - session_id: ", session_id)
nano.fini_session(session_data)
kong.ctx.plugin.blocked = true
local result = nano.handle_custom_response(session_data, response)
nano.fini_session(session_data)
nano.cleanup_all()
kong.ctx.plugin.session_data = nil
kong.ctx.plugin.session_id = nil
return result
end
@@ -90,315 +60,172 @@ function NanoHandler.access(conf)
if body and #body > 0 then
verdict, response = nano.send_body(session_id, session_data, body, nano.HttpChunkType.HTTP_REQUEST_BODY)
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in access/send_body (raw) - session_id: ", session_id)
nano.fini_session(session_data)
kong.ctx.plugin.blocked = true
local result = nano.handle_custom_response(session_data, response)
nano.fini_session(session_data)
nano.cleanup_all()
kong.ctx.plugin.session_data = nil
kong.ctx.plugin.session_id = nil
return result
end
-- Free body from memory after sending
body = nil
collectgarbage("step", 100)
else
kong.log.err("Request body not in memory, attempting to read from buffer/file")
kong.log.debug("Request body not in memory, attempting to read from buffer/file")
local body_data = ngx.var.request_body
if body_data and #body_data > 0 then
kong.log.err("Found request body in nginx var, size: ", #body_data)
kong.log.debug("Found request body in nginx var, size: ", #body_data)
verdict, response = nano.send_body(session_id, session_data, body_data, nano.HttpChunkType.HTTP_REQUEST_BODY)
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in access/send_body (var) - session_id: ", session_id)
kong.ctx.plugin.blocked = true
local result = nano.handle_custom_response(session_data, response)
nano.fini_session(session_data)
nano.cleanup_all()
kong.ctx.plugin.session_data = nil
kong.ctx.plugin.session_id = nil
return result
kong.ctx.plugin.blocked = true
return nano.handle_custom_response(session_data, response)
end
-- Free body_data from memory
body_data = nil
collectgarbage("step", 100)
else
local body_file = ngx.var.request_body_file
if body_file then
kong.log.err("Reading request body from file: ", body_file)
kong.log.debug("Reading request body from file: ", body_file)
local file = io.open(body_file, "rb")
if file then
local entire_body = file:read("*all")
file:close()
if entire_body and #entire_body > 0 then
kong.log.err("Sending entire body of size ", #entire_body, " bytes to C module")
kong.log.debug("Sending entire body of size ", #entire_body, " bytes to C module")
verdict, response = nano.send_body(session_id, session_data, entire_body, nano.HttpChunkType.HTTP_REQUEST_BODY)
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in access/send_body (file) - session_id: ", session_id)
nano.fini_session(session_data)
kong.ctx.plugin.blocked = true
local result = nano.handle_custom_response(session_data, response)
nano.fini_session(session_data)
nano.cleanup_all()
kong.ctx.plugin.session_data = nil
kong.ctx.plugin.session_id = nil
return result
end
-- Free entire_body from memory
entire_body = nil
collectgarbage("step", 100)
else
kong.log.err("Empty body file")
kong.log.debug("Empty body file")
end
end
else
kong.log.err("Request body expected but no body data or file available")
kong.log.warn("Request body expected but no body data or file available")
end
end
end
end
-- End request inspection
local ok, verdict, response = pcall(function()
return nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_REQUEST_END)
end)
local ok, verdict, response = pcall(function()
return nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_REQUEST_END)
end)
if not ok then
kong.log.err("Error ending request inspection: ", verdict, " - failing open")
nano.fini_session(session_data)
nano.cleanup_all()
collectgarbage("collect")
kong.ctx.plugin.session_id = nil
kong.ctx.plugin.session_data = nil
return
end
if not ok then
kong.log.err("Error ending request inspection: ", verdict, " - failing open")
nano.fini_session(session_data)
nano.cleanup_all()
return
end
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in access/end_inspection - session_id: ", session_id)
kong.ctx.plugin.blocked = true
local result = nano.handle_custom_response(session_data, response)
nano.fini_session(session_data)
nano.cleanup_all()
kong.ctx.plugin.session_data = nil
kong.ctx.plugin.session_id = nil
return result
if verdict == nano.AttachmentVerdict.DROP then
nano.fini_session(session_data)
kong.ctx.plugin.blocked = true
local result = nano.handle_custom_response(session_data, response)
nano.cleanup_all()
return result
end
else
verdict, response = nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_REQUEST_END)
if verdict == nano.AttachmentVerdict.DROP then
nano.fini_session(session_data)
kong.ctx.plugin.blocked = true
local result = nano.handle_custom_response(session_data, response)
nano.cleanup_all()
return result
end
end
NanoHandler.processed_requests[session_id] = true
end
function NanoHandler.header_filter(conf)
kong.log.debug("2-HEADER_FILTER PHASE START")
local ctx = kong.ctx.plugin
if ctx.blocked then
return
end
if not ctx.session_id or not ctx.session_data then
kong.log.debug("No session data in header_filter")
local session_id = ctx.session_id
local session_data = ctx.session_data
if not session_id or not session_data then
kong.log.err("No session data found in header_filter")
return
end
local headers = kong.response.get_headers()
local header_data = nano.handleHeaders(headers)
local status_code = kong.response.get_status()
local content_length = tonumber(headers["content-length"]) or 0
local ok, verdict, response = pcall(function()
return nano.send_response_headers(ctx.session_id, ctx.session_data, nano.handleHeaders(headers), status_code, content_length)
end)
if not ok then
kong.log.err("send_response_headers failed: ", tostring(verdict))
nano.fini_session(ctx.session_data)
nano.cleanup_all()
collectgarbage("collect")
ctx.session_id = nil
ctx.session_data = nil
return
end
local verdict, response = nano.send_response_headers(session_id, session_data, header_data, status_code, content_length)
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in header_filter - session_id: ", ctx.session_id)
ctx.blocked = true
local result = nano.handle_custom_response(ctx.session_data, response)
nano.fini_session(ctx.session_data)
kong.ctx.plugin.blocked = true
nano.fini_session(session_data)
nano.cleanup_all()
ctx.session_data = nil
ctx.session_id = nil
return result
elseif verdict == nano.AttachmentVerdict.ACCEPT then
kong.log.debug("ACCEPT verdict in header_filter - marking inspection complete")
return nano.handle_custom_response(session_data, response)
end
ctx.expect_body = not (status_code == 204 or status_code == 304 or (100 <= status_code and status_code < 200) or content_length == 0)
end
function NanoHandler.body_filter(conf)
local ctx = kong.ctx.plugin
local eof = ngx.arg[2]
-- Log first chunk only
if not ctx.body_filter_start_time then
kong.log.debug("3-BODY_FILTER PHASE START")
ctx.body_filter_start_time = ngx.now() * 1000
end
-- Fast path: skip if already blocked
if ctx.blocked then
ngx.arg[1] = nil -- Discard chunk if blocked
collectgarbage("step", 100)
return
end
if not ctx.session_id or not ctx.session_data then
ngx.arg[1] = nil
collectgarbage("step", 100)
local session_id = ctx.session_id
local session_data = ctx.session_data
if not session_id or not session_data or ctx.session_finalized then
return
end
-- CRITICAL: Check if session is finalized (exactly like Envoy does)
-- This prevents sending chunks after final verdict received
if nano.is_session_finalized(ctx.session_data) then
kong.log.debug("Session already finalized - skipping inspection")
local body = kong.response.get_raw_body()
if body then
ctx.body_seen = true
local verdict, response, modifications = nano.send_body(session_id, session_data, body, nano.HttpChunkType.HTTP_RESPONSE_BODY)
-- Initialize if not exists
ctx.body_buffer_chunk = ctx.body_buffer_chunk or 0
-- Handle body modifications if any
if modifications then
body = nano.handle_body_modifications(body, modifications, ctx.body_buffer_chunk)
kong.response.set_raw_body(body)
end
ctx.body_buffer_chunk = ctx.body_buffer_chunk + 1
if verdict == nano.AttachmentVerdict.DROP then
nano.fini_session(session_data)
ctx.session_finalized = true
local result = nano.handle_custom_response(session_data, response)
-- Clean up allocated memory
nano.cleanup_all()
return result
end
return
end
-- Check timeout (150 seconds)
local elapsed = (ngx.now() * 1000) - ctx.body_filter_start_time
if elapsed > 150000 then
kong.log.err("Timeout after ", elapsed, "ms - cleaning up session")
ngx.arg[1] = nil -- Discard chunk first
nano.fini_session(ctx.session_data)
if ctx.body_seen or ctx.expect_body == false then
local verdict, response = nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_RESPONSE_END)
if verdict == nano.AttachmentVerdict.DROP then
nano.fini_session(session_data)
ctx.session_finalized = true
local result = nano.handle_custom_response(session_data, response)
-- Clean up allocated memory
nano.cleanup_all()
return result
end
nano.fini_session(session_data)
-- Clean up allocated memory
nano.cleanup_all()
collectgarbage("collect")
ctx.session_id = nil
ctx.session_data = nil
return
end
-- Read chunk for active inspection
local chunk = ngx.arg[1]
if chunk and #chunk > 0 then
local ok, result = pcall(function()
return {nano.send_body(ctx.session_id, ctx.session_data, chunk, nano.HttpChunkType.HTTP_RESPONSE_BODY)}
end)
if ok then
local verdict = result[1]
local response = result[2]
local modifications = result[3]
if modifications then
chunk = nano.handle_body_modifications(chunk, modifications, 0)
ngx.arg[1] = chunk
end
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in body_filter/send_body - session_id: ", ctx.session_id)
ctx.blocked = true
local result = nano.handle_custom_response(ctx.session_data, response)
nano.fini_session(ctx.session_data)
nano.cleanup_all()
ctx.session_data = nil
ctx.session_id = nil
return result
elseif verdict == nano.AttachmentVerdict.ACCEPT then
-- Final ACCEPT verdict received - mark complete but don't cleanup yet (wait for EOF)
kong.log.debug("ACCEPT verdict received - session finalized")
end
-- Incremental GC after processing chunk
chunk = nil
collectgarbage("step", 100)
else
kong.log.err("nano.send_body failed: ", tostring(result), " - cleaning up session")
nano.fini_session(ctx.session_data)
nano.cleanup_all()
collectgarbage("collect")
ctx.session_id = nil
ctx.session_data = nil
return
end
end
-- Process EOF
if eof then
-- Only send end_inspection if we haven't already finalized
if not nano.is_session_finalized(ctx.session_data) then
local ok, result = pcall(function()
return {nano.end_inspection(ctx.session_id, ctx.session_data, nano.HttpChunkType.HTTP_RESPONSE_END)}
end)
if ok then
local verdict = result[1]
local response = result[2]
if verdict == nano.AttachmentVerdict.DROP then
kong.log.err("DROP verdict in body_filter/end_inspection - session_id: ", ctx.session_id)
ctx.blocked = true
local result = nano.handle_custom_response(ctx.session_data, response)
nano.fini_session(ctx.session_data)
nano.cleanup_all()
ctx.session_data = nil
ctx.session_id = nil
return result
end
else
kong.log.err("nano.end_inspection failed: ", tostring(result), " - cleaning up session")
end
end
-- CRITICAL: Always cleanup session at EOF, even if inspection_complete is true
-- This ensures ACCEPT verdict sessions get cleaned up
if ctx.session_data then
nano.fini_session(ctx.session_data)
nano.cleanup_all()
collectgarbage("collect")
ctx.session_id = nil
ctx.session_data = nil
end
ctx.session_finalized = true
end
end
function NanoHandler.log(conf)
kong.log.debug("4-LOG PHASE START")
local ctx = kong.ctx.plugin
-- Cleanup blocked sessions that returned early from access phase
if ctx.blocked and ctx.session_data then
kong.log.err("Cleaning up blocked session ", ctx.session_id)
nano.fini_session(ctx.session_data)
nano.cleanup_all()
ctx.session_data = nil
ctx.session_id = nil
end
-- Force GC if memory is high
local mem_before = collectgarbage("count")
if mem_before > 10240 then
kong.log.err("High memory: ", string.format("%.2f", mem_before), " KB - forcing GC")
collectgarbage("collect")
local mem_after = collectgarbage("count")
kong.log.err("Memory after GC: ", string.format("%.2f", mem_after), " KB (freed ", string.format("%.2f", mem_before - mem_after), " KB)")
end
-- Log memory periodically
if ngx.worker.id() == 0 then
local request_count = ngx.shared.kong_cache and ngx.shared.kong_cache:incr("request_count", 1, 0) or 0
if request_count % 100 == 0 then
local mem_kb = collectgarbage("count")
kong.log.err("Lua memory: ", string.format("%.2f", mem_kb), " KB")
end
end
-- Emergency cleanup if body_filter never completed
if ctx.session_id and ctx.session_data then
kong.log.err("Emergency cleanup for session ", ctx.session_id)
nano.fini_session(ctx.session_data)
collectgarbage("collect")
ctx.session_id = nil
ctx.session_data = nil
end
end
return NanoHandler
return NanoHandler

View File

@@ -6,8 +6,6 @@ local nano = {}
nano.session_counter = 0
nano.attachments = {}
nano.num_workers = ngx.worker.count() or 1
nano.last_init_attempt = {} -- Track timestamp of last init attempt per worker
nano.init_backoff_seconds = 5 -- Wait 5 seconds between retry attempts
nano.allocated_strings = {}
nano.allocate_headers = {}
nano.allocated_metadata = {}
@@ -94,7 +92,6 @@ function nano.handle_custom_response(session_data, response)
end
local response_type = nano_attachment.get_web_response_type(attachment, session_data, response)
kong.log.err("Block response - type: ", response_type)
if response_type == nano.WebResponseType.RESPONSE_CODE_ONLY then
local code = nano_attachment.get_response_code(response)
@@ -102,13 +99,12 @@ function nano.handle_custom_response(session_data, response)
kong.log.warn("Invalid response code received: ", code, " - using 403 instead")
code = 403
end
kong.log.err("Response code only: ", code)
kong.log.debug("Response code only: ", code)
return kong.response.exit(code, "")
end
if response_type == nano.WebResponseType.REDIRECT_WEB_RESPONSE then
local location = nano_attachment.get_redirect_page(attachment, session_data, response)
kong.log.err("Redirect response to: ", location)
return kong.response.exit(307, "", { ["Location"] = location })
end
@@ -122,8 +118,9 @@ function nano.handle_custom_response(session_data, response)
kong.log.warn("Invalid response code received: ", code, " - using 403 instead")
code = 403
end
kong.log.err("Block page response with code: ", code, ", page length: ", #block_page)
kong.log.debug("Block page response with code: ", code)
return kong.response.exit(code, block_page, { ["Content-Type"] = "text/html" })
end
@@ -172,13 +169,6 @@ function nano.free_all_responses()
nano.allocated_responses = {}
end
-- Free a single response immediately (for INSPECT/ACCEPT verdicts)
function nano.free_response_immediate(response)
if response then
nano_attachment.free_verdict_response(response)
end
end
function nano.cleanup_all()
nano.free_all_nano_str()
nano.free_all_metadata()
@@ -188,20 +178,6 @@ end
function nano.init_attachment()
local worker_id = ngx.worker.id()
-- Check if we should throttle retry attempts
local last_attempt = nano.last_init_attempt[worker_id]
if last_attempt then
local time_since_last = ngx.now() - last_attempt
if time_since_last < nano.init_backoff_seconds then
-- Too soon to retry, skip this attempt
return false
end
end
-- Record this attempt timestamp
nano.last_init_attempt[worker_id] = ngx.now()
local attachment, err
local retries = 3
@@ -215,11 +191,9 @@ function nano.init_attachment()
end
if not attachment then
kong.log.err("Worker ", worker_id, " failed to initialize attachment after ", retries, " attempts. Worker will operate in fail-open mode. Will retry in ", nano.init_backoff_seconds, " seconds.")
return false
kong.log.err("Worker ", worker_id, " failed to initialize attachment after ", retries, " attempts. Worker will operate in fail-open mode.")
else
nano.attachments[worker_id] = attachment
nano.last_init_attempt[worker_id] = nil -- Clear backoff on success
kong.log.info("Worker ", worker_id, " successfully initialized nano_attachment.")
return true
end
@@ -231,18 +205,9 @@ function nano.init_session(session_id)
if not attachment then
kong.log.warn("Attachment not found for worker ", worker_id, ", attempting to reinitialize...")
local init_success = nano.init_attachment()
if not init_success then
-- Log only if this is a fresh retry (not throttled)
local last_attempt = nano.last_init_attempt[worker_id]
if last_attempt and (ngx.now() - last_attempt) < 1 then
kong.log.warn("Cannot initialize session: Attachment initialization just attempted for worker ", worker_id, " - failing open. Will retry in ", nano.init_backoff_seconds, " seconds.")
end
return nil
end
nano.init_attachment()
attachment = nano.attachments[worker_id]
if not attachment then
kong.log.warn("Cannot initialize session: Attachment still not available for worker ", worker_id, " - failing open")
return nil
@@ -447,33 +412,6 @@ function nano.send_response_headers(session_id, session_data, headers, status_co
return verdict, response
end
function nano.send_content_length(session_id, session_data, content_length)
local worker_id = ngx.worker.id()
local attachment = nano.attachments[worker_id]
if not attachment then
kong.log.warn("Attachment not available for worker ", worker_id, " - failing open")
return nano.AttachmentVerdict.INSPECT
end
local verdict, response = nano_attachment.send_content_length(
attachment,
session_id,
session_data,
content_length
)
if response then
if verdict == nano.AttachmentVerdict.DROP then
table.insert(nano.allocated_responses, response)
else
nano.free_response_immediate(response)
end
end
return verdict, response
end
function nano.handle_header_modifications(headers, modifications)
if not modifications then
return headers
@@ -543,4 +481,4 @@ function nano.end_inspection(session_id, session_data, chunk_type)
return verdict, response
end
return nano
return nano