|
4 | 4 | //! `proxy_on_context_create`, `proxy_on_request_headers`, |
5 | 5 | //! `proxy_on_request_body`, `proxy_on_response_headers`, |
6 | 6 | //! `proxy_on_done`. Request headers fire the "allow" target rule; |
| 7 | +//! request body fires "allow_body" with `{"body": <parsed-json>, |
| 8 | +//! "body_raw": <string>}` once the host signals end of stream; |
7 | 9 | //! response headers fire "allow_response" with `{"response":{...}}`. |
8 | | -//! Body callbacks are no-ops; see ROADMAP.md. |
9 | 10 | //! |
10 | 11 | //! Configuration: the policy AST JSON arrives via |
11 | 12 | //! `proxy_on_configure`. We copy it into `host_allocator` so it |
|
16 | 17 | //! `malloc`. We `hostFree` them once consumed. |
17 | 18 |
|
18 | 19 | const std = @import("std"); |
19 | | -const memory = @import("memory.zig"); |
20 | 20 | const eval = @import("eval.zig"); |
| 21 | +const json = @import("json.zig"); |
| 22 | +const memory = @import("memory.zig"); |
21 | 23 |
|
22 | 24 | // ABI version negotiation: one empty export per supported version. |
23 | 25 |
|
@@ -144,12 +146,88 @@ export fn proxy_on_request_headers(_: i32, _: i32, _: i32) i32 { |
144 | 146 | return action_continue; |
145 | 147 | } |
146 | 148 |
|
147 | | -/// No-op for now. Keeps the symbol resolvable when the filter |
148 | | -/// declares body interest. |
149 | | -export fn proxy_on_request_body(_: i32, _: i32, _: i32) i32 { |
| 149 | +/// Evaluate against the request body once the host signals end of |
| 150 | +/// stream. Until then we return `Continue` so streaming chunks pass |
| 151 | +/// through; the final fragment triggers the eval. Body input shape |
| 152 | +/// is `{"body": <parsed-or-null>, "body_raw": <string>}`. |
| 153 | +/// |
| 154 | +/// Hosts clear `:method` / `:path` from the header map by the time |
| 155 | +/// this fires (Envoy/wamr behaviour), so a body rule that needs |
| 156 | +/// header context must depend on a snapshot taken in |
| 157 | +/// `proxy_on_request_headers`. Per-context snapshot plumbing is |
| 158 | +/// tracked in ROADMAP.md; v1 surfaces only the body itself. |
| 159 | +export fn proxy_on_request_body(_: i32, body_size: i32, end_of_stream: i32) i32 { |
| 160 | + if (end_of_stream == 0) return action_continue; |
| 161 | + if (body_size <= 0) return action_continue; |
| 162 | + const policy = configured_policy orelse return action_continue; |
| 163 | + if (!evaluateBodyAt(@intCast(body_size), policy)) { |
| 164 | + denyWithStatus(403); |
| 165 | + return action_pause; |
| 166 | + } |
150 | 167 | return action_continue; |
151 | 168 | } |
152 | 169 |
|
| 170 | +const body_target_rule: []const u8 = "allow_body"; |
| 171 | +const max_body_bytes: usize = 64 * 1024; |
| 172 | + |
| 173 | +fn evaluateBodyAt(body_size: usize, policy: []const u8) bool { |
| 174 | + const arena = memory.requestArena(); |
| 175 | + defer memory.resetRequestArena(); |
| 176 | + const allocator = arena.allocator(); |
| 177 | + |
| 178 | + const cap = if (body_size > max_body_bytes) max_body_bytes else body_size; |
| 179 | + const body_bytes = readBodyBytes(allocator, cap) catch return false; |
| 180 | + const input_bytes = buildBodyInput(allocator, body_bytes) catch return false; |
| 181 | + return eval.evaluateWithTarget(arena, input_bytes, policy, body_target_rule) catch false; |
| 182 | +} |
| 183 | + |
| 184 | +/// Pull the request body from the host. Returns an empty slice on |
| 185 | +/// host error so the caller sees a body of "" rather than failing |
| 186 | +/// the request outright. |
| 187 | +fn readBodyBytes(allocator: std.mem.Allocator, cap: usize) ![]const u8 { |
| 188 | + var data: ?[*]u8 = null; |
| 189 | + var data_size: usize = 0; |
| 190 | + const status = proxy_get_buffer_bytes( |
| 191 | + buffer_type_http_request_body, |
| 192 | + 0, |
| 193 | + cap, |
| 194 | + &data, |
| 195 | + &data_size, |
| 196 | + ); |
| 197 | + if (status != status_ok) return &[_]u8{}; |
| 198 | + if (data_size == 0) return &[_]u8{}; |
| 199 | + const ptr = data orelse return &[_]u8{}; |
| 200 | + defer memory.hostFree(ptr); |
| 201 | + return try allocator.dupe(u8, ptr[0..data_size]); |
| 202 | +} |
| 203 | + |
| 204 | +/// Build `{"body": <parsed-json-or-null>, "body_raw": <string>}`. We |
| 205 | +/// try to parse the body as JSON; if it fails, `body` is null and |
| 206 | +/// the policy can still match against `body_raw` (e.g. with the |
| 207 | +/// `contains` builtin). The parsed copy is dropped on the next |
| 208 | +/// arena reset, so this only costs one transient walk. |
| 209 | +fn buildBodyInput(allocator: std.mem.Allocator, body: []const u8) ![]u8 { |
| 210 | + const parsed_ok = blk: { |
| 211 | + _ = json.parse(allocator, body) catch break :blk false; |
| 212 | + break :blk true; |
| 213 | + }; |
| 214 | + |
| 215 | + var buf: std.ArrayList(u8) = .empty; |
| 216 | + defer buf.deinit(allocator); |
| 217 | + |
| 218 | + try buf.appendSlice(allocator, "{\"body\":"); |
| 219 | + if (parsed_ok and body.len > 0) { |
| 220 | + try buf.appendSlice(allocator, body); |
| 221 | + } else { |
| 222 | + try buf.appendSlice(allocator, "null"); |
| 223 | + } |
| 224 | + try buf.appendSlice(allocator, ",\"body_raw\":"); |
| 225 | + try appendJsonString(allocator, &buf, body); |
| 226 | + try buf.append(allocator, '}'); |
| 227 | + |
| 228 | + return try allocator.dupe(u8, buf.items); |
| 229 | +} |
| 230 | + |
153 | 231 | /// Evaluate against response status + headers under the |
154 | 232 | /// `allow_response` target rule. Deny replaces the response with a |
155 | 233 | /// 503; allow lets the upstream response through unchanged. |
|
0 commit comments