From f0c3dd8efcd1f34fb1d76e4242ba942e189400b6 Mon Sep 17 00:00:00 2001 From: wiaamm Date: Wed, 19 Nov 2025 13:58:09 +0200 Subject: [PATCH] fix response body --- .../open-appsec-waf-kong-plugin/handler.lua | 209 ++++-------------- 1 file changed, 43 insertions(+), 166 deletions(-) diff --git a/attachments/kong/plugins/open-appsec-waf-kong-plugin/handler.lua b/attachments/kong/plugins/open-appsec-waf-kong-plugin/handler.lua index d19af1b..5b2a269 100755 --- a/attachments/kong/plugins/open-appsec-waf-kong-plugin/handler.lua +++ b/attachments/kong/plugins/open-appsec-waf-kong-plugin/handler.lua @@ -140,7 +140,6 @@ end function NanoHandler.header_filter(conf) local ctx = kong.ctx.plugin if ctx.blocked then - kong.log.debug("[header_filter] Blocked context, returning early") return end @@ -148,7 +147,7 @@ function NanoHandler.header_filter(conf) local session_data = ctx.session_data if not session_id or not session_data then - kong.log.err("[header_filter] No session data found in header_filter") + kong.log.err("No session data found in header_filter") return end @@ -157,206 +156,84 @@ function NanoHandler.header_filter(conf) local status_code = kong.response.get_status() local content_length = tonumber(headers["content-length"]) or 0 - kong.log.debug("[header_filter] Session: ", session_id, " | Status: ", status_code, " | Content-Length: ", content_length) - - -- Send response headers WITHOUT content_length - -- Pass 0 to indicate we'll send body in chunks via body_filter - local verdict, response = nano.send_response_headers(session_id, session_data, header_data, status_code, 0) - - kong.log.debug("[header_filter] Session: ", session_id, " | Response headers verdict: ", verdict) - + 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.warn("[header_filter] Response headers verdict DROP for session: ", session_id) kong.ctx.plugin.blocked = true nano.fini_session(session_data) nano.cleanup_all() return nano.handle_custom_response(session_data, response) end - -- DO NOT send content_length separately - it causes nano service to block waiting for that many bytes - -- Instead, let body_filter send chunks progressively without pre-declaring the total size - - -- If nano service returned ACCEPT verdict, it means it's done inspecting and doesn't want response body - -- Skip body_filter to avoid timeout cascades from sending unwanted data - if verdict == nano.AttachmentVerdict.ACCEPT then - kong.log.info("[header_filter] Session: ", session_id, " | Verdict ACCEPT - will skip response body inspection") - ctx.skip_body_filter = true - - -- Disable proxy buffering to stream the response directly to client without temp files - -- This prevents "out of memory" issues with large response bodies - ngx.var.upstream_no_cache = "1" -- Disable caching - kong.response.set_header("X-Accel-Buffering", "no") -- Disable nginx buffering - - -- DON'T finalize session here - let body_filter handle it at EOF - -- Finalizing here may block the body data flow - end - ctx.expect_body = not (status_code == 204 or status_code == 304 or (100 <= status_code and status_code < 200) or content_length == 0) - - kong.log.debug("[header_filter] Session: ", session_id, " | Expect body: ", ctx.expect_body) end function NanoHandler.body_filter(conf) local ctx = kong.ctx.plugin - if ctx.blocked then - kong.log.debug("[body_filter] Blocked context, returning early") + if not ctx or ctx.blocked then return end - local session_id = ctx.session_id + local session_id = ctx.session_id local session_data = ctx.session_data - if not session_id or not session_data then - kong.log.debug("[body_filter] No session_id or session_data, returning early") + if not session_id or not session_data or ctx.session_finalized then return end - if ctx.session_finalized then - kong.log.debug("[body_filter] Session already finalized for session: ", session_id, ", returning early") - return - end - local chunk = ngx.arg[1] - local eof = ngx.arg[2] - - -- If nano service already accepted the response in header_filter, skip body inspection - -- Just let chunks pass through immediately and finalize at EOF - if ctx.skip_body_filter then - -- Initialize counter on first skip - if not ctx.skipped_chunks then - ctx.skipped_chunks = 0 - ctx.skipped_bytes = 0 - end - - ctx.skipped_chunks = ctx.skipped_chunks + 1 - ctx.skipped_bytes = ctx.skipped_bytes + (chunk and #chunk or 0) - - if ctx.skipped_chunks % 100 == 1 then -- Log every 100 chunks - kong.log.info("[body_filter] Session: ", session_id, " | Skipped ", ctx.skipped_chunks, " chunks (", ctx.skipped_bytes, " bytes), EOF: ", tostring(eof)) - end - - -- If there were any buffered chunks from before skip was set, flush them first - if ctx.chunk_buffer and #ctx.chunk_buffer > 0 then - kong.log.info("[body_filter] Session: ", session_id, " | Flushing ", #ctx.chunk_buffer, " buffered chunks before skipping") - local combined = table.concat(ctx.chunk_buffer) - ctx.chunk_buffer = {} - ctx.chunk_buffer_size = 0 - -- Send the buffered data followed by current chunk - if chunk and #chunk > 0 then - ngx.arg[1] = combined .. chunk - else - ngx.arg[1] = combined - end - end - -- Chunk passes through as-is via ngx.arg[1] - don't modify it - - if eof then - kong.log.info("[body_filter] Session: ", session_id, " | EOF reached after skipping ", ctx.skipped_chunks, " chunks (", ctx.skipped_bytes, " bytes total)") - nano.fini_session(session_data) - nano.cleanup_all() - ctx.session_finalized = true - end - return - end + local eof = ngx.arg[2] - kong.log.debug("[body_filter] Session: ", session_id, " | Chunk size: ", chunk and #chunk or 0, " | EOF: ", tostring(eof)) - - -- Initialize on first call - if not ctx.chunk_buffer then - ctx.body_buffer_chunk = 0 - ctx.chunk_buffer = {} - ctx.chunk_buffer_size = 0 - ctx.consecutive_inspect_verdicts = 0 -- Track if nano service is actually processing - end - - -- Batch configuration: combine small chunks to reduce nano service calls - local MAX_BATCH_SIZE = 64 * 1024 -- 64KB batches - local MAX_CONSECUTIVE_INSPECTS = 10 -- If we get 10 INSPECT verdicts in a row, assume nano wants full inspection - - -- Process current chunk if present + -- Handle body chunks if chunk and #chunk > 0 then - -- Add chunk to buffer - table.insert(ctx.chunk_buffer, chunk) - ctx.chunk_buffer_size = ctx.chunk_buffer_size + #chunk - - local should_send = false - - -- Send if: batch full or EOF coming - if ctx.chunk_buffer_size >= MAX_BATCH_SIZE or eof then - should_send = true + ctx.body_seen = true + + -- Initialize chunk index if not exists + if not ctx.body_buffer_chunk then + ctx.body_buffer_chunk = 0 end - - if should_send and #ctx.chunk_buffer > 0 then - -- Combine buffered chunks - local combined_chunk = table.concat(ctx.chunk_buffer) - - kong.log.debug("[body_filter] Session: ", session_id, " | Sending batched chunk #", ctx.body_buffer_chunk, - ", size: ", #combined_chunk, " bytes (", #ctx.chunk_buffer, " chunks combined)") - - local verdict, response, modifications = nano.send_body(session_id, session_data, combined_chunk, nano.HttpChunkType.HTTP_RESPONSE_BODY) - kong.log.debug("[body_filter] Session: ", session_id, " | Verdict after chunk #", ctx.body_buffer_chunk, ": ", verdict) + local verdict, response, modifications = + nano.send_body(session_id, session_data, chunk, nano.HttpChunkType.HTTP_RESPONSE_BODY) - if modifications then - kong.log.debug("[body_filter] Session: ", session_id, " | Applying body modifications to chunk") - combined_chunk = nano.handle_body_modifications(combined_chunk, modifications, ctx.body_buffer_chunk) - ngx.arg[1] = combined_chunk - else - ngx.arg[1] = combined_chunk - end - - ctx.body_buffer_chunk = ctx.body_buffer_chunk + 1 - ctx.body_seen = true - - -- Clear buffer - ctx.chunk_buffer = {} - ctx.chunk_buffer_size = 0 - - if verdict == nano.AttachmentVerdict.DROP then - kong.log.warn("[body_filter] Body chunk verdict DROP for session: ", session_id) - nano.fini_session(session_data) - ctx.session_finalized = true - local result = nano.handle_custom_response(session_data, response) - nano.cleanup_all() - return result - elseif verdict == nano.AttachmentVerdict.ACCEPT then - -- Nano service is done inspecting, stop sending more chunks - kong.log.info("[body_filter] Session: ", session_id, " | Verdict ACCEPT after chunk #", ctx.body_buffer_chunk - 1, " - stopping body inspection") - ctx.skip_body_filter = true - nano.fini_session(session_data) - ctx.session_finalized = true - -- Let remaining chunks pass through without inspection - end - else - -- Buffering chunk, don't send to client yet - kong.log.debug("[body_filter] Session: ", session_id, " | Buffering chunk (", #chunk, " bytes), total buffered: ", ctx.chunk_buffer_size) - ngx.arg[1] = nil -- Don't send this chunk to client yet + -- Handle body modifications if any + if modifications then + chunk = nano.handle_body_modifications(chunk, modifications, ctx.body_buffer_chunk) + ngx.arg[1] = chunk end - end - -- End inspection at EOF - if eof then - kong.log.debug("[body_filter] Session: ", session_id, " | EOF reached, body_seen: ", tostring(ctx.body_seen), ", chunks processed: ", ctx.body_buffer_chunk) - - kong.log.debug("[body_filter] Session: ", session_id, " | Ending inspection") - local verdict, response = nano.end_inspection(session_id, session_data, nano.HttpChunkType.HTTP_RESPONSE_END) - - kong.log.debug("[body_filter] Session: ", session_id, " | End inspection verdict: ", verdict) - + ctx.body_buffer_chunk = ctx.body_buffer_chunk + 1 + if verdict == nano.AttachmentVerdict.DROP then - kong.log.warn("[body_filter] End inspection verdict DROP for session: ", session_id) nano.fini_session(session_data) ctx.session_finalized = true local result = nano.handle_custom_response(session_data, response) nano.cleanup_all() + -- Stop current streaming + ngx.arg[1] = "" + ngx.arg[2] = true return result end + end - kong.log.debug("[body_filter] Session: ", session_id, " | Finalizing session normally") - nano.fini_session(session_data) - nano.cleanup_all() - ctx.session_finalized = true + -- Handle end of response + if eof then + 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) + nano.cleanup_all() + ngx.arg[1] = "" + ngx.arg[2] = true + return result + end + + nano.fini_session(session_data) + nano.cleanup_all() + ctx.session_finalized = true + end end end -return NanoHandler +return NanoHandler \ No newline at end of file