From 1f9fd197719c254d951eb67f519a4ef118ddec70 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Wed, 31 May 2023 10:18:09 -0700 Subject: [PATCH] implement header matching in proxy.ini --- src/interface.zig | 12 ++++++++++++ src/main-lib.zig | 11 ++++++++++- src/main.zig | 46 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/interface.zig b/src/interface.zig index 42c07d0..c4e2d91 100644 --- a/src/interface.zig +++ b/src/interface.zig @@ -38,6 +38,11 @@ pub const ZigRequest = struct { headers: []Header, }; +pub const ZigHeader = struct { + name: []u8, + value: []u8, +}; + pub const ZigResponse = struct { body: *std.ArrayList(u8), headers: *std.StringHashMap([]const u8), @@ -55,6 +60,13 @@ pub fn zigInit(parent_allocator: *anyopaque) callconv(.C) void { allocator = @ptrCast(*std.mem.Allocator, @alignCast(@alignOf(*std.mem.Allocator), parent_allocator)); } +pub fn toZigHeader(header: Header) ZigHeader { + return .{ + .name = header.name_ptr[0..header.name_len], + .value = header.value_ptr[0..header.value_len], + }; +} + /// Converts a StringHashMap to the structure necessary for passing through the /// C boundary. This will be called automatically for you via the handleRequest function /// and is also used by the main processing loop to coerce request headers diff --git a/src/main-lib.zig b/src/main-lib.zig index 2d63bb6..14ff682 100644 --- a/src/main-lib.zig +++ b/src/main-lib.zig @@ -40,7 +40,16 @@ fn handleRequest(allocator: std.mem.Allocator, request: interface.ZigRequest, re // setup var response_writer = response.body.writer(); // real work - response_writer.print(" {d}", .{request.headers.len}) catch unreachable; + for (request.headers) |h| { + const header = interface.toZigHeader(h); + if (!std.ascii.eqlIgnoreCase(header.name, "host")) continue; + if (std.mem.startsWith(u8, header.value, "iam")) { + try response_writer.print("iam response", .{}); + return; + } + break; + } + try response_writer.print(" {d}", .{request.headers.len}); try response.headers.put("X-custom-foo", "bar"); log.info("handlerequest header count {d}", .{response.headers.count()}); } diff --git a/src/main.zig b/src/main.zig index c680bff..bfd1697 100644 --- a/src/main.zig +++ b/src/main.zig @@ -37,7 +37,7 @@ const FullReturn = struct { // applies const Executor = struct { // configuration - target_prefix: []const u8, + match_data: []const u8, path: [:0]const u8, // fields used at runtime to do real work @@ -62,8 +62,7 @@ var parsed_config: config.ParsedConfig = undefined; /// Serves a single request. Finds executor, marshalls request data for the C /// interface, calls the executor and marshalls data back fn serve(allocator: *std.mem.Allocator, response: *std.http.Server.Response) !*FullReturn { - // if (some path routing thing) { - const executor = try getExecutor(response.request.target); + const executor = try getExecutor(response.request.target, response.request.headers); if (executor.zigInitFn) |f| f(allocator); @@ -115,10 +114,10 @@ fn serve(allocator: *std.mem.Allocator, response: *std.http.Server.Response) !*F } /// Gets and executor based on request data -fn getExecutor(requested_path: []const u8) !*Executor { +fn getExecutor(requested_path: []const u8, headers: std.http.Headers) !*Executor { var executor = blk: { for (executors) |*exec| { - if (std.mem.startsWith(u8, requested_path, exec.target_prefix)) { + if (executorIsMatch(exec.match_data, requested_path, headers)) { break :blk exec; } } @@ -154,6 +153,41 @@ fn getExecutor(requested_path: []const u8) !*Executor { return executor; } +fn executorIsMatch(match_data: []const u8, requested_path: []const u8, headers: std.http.Headers) bool { + if (!std.mem.containsAtLeast(u8, match_data, 1, ":")) { + // match_data does not have a ':'. This means this is a straight path, without + // any header requirement. We can simply return a match prefix on the + // requested path + const rc = std.mem.startsWith(u8, requested_path, match_data); + if (rc) log.debug("executor match for path prefix '{s}'", .{match_data}); + return rc; + } + const colon = std.mem.indexOf(u8, match_data, ":").?; + const header_needle = match_data[0..colon]; + const header_inx = headers.firstIndexOf(header_needle) orelse return false; + // Apparently std.mem.split will return an empty first when the haystack starts + // with the delimiter + var split = std.mem.split(u8, std.mem.trim(u8, match_data[colon + 1 ..], "\t "), " "); + const header_value_needle = split.first(); + const path_needle = split.next() orelse { + std.log.warn( + "Incorrect configuration. Header matching requires both header value and path prefix, space delimited. Key was '{s}'", + .{match_data}, + ); + return false; + }; + // match_data includes some sort of header match as well. We assume the + // header match is a full match on the key (handled above) + // but a prefix match on the value + const request_header_value = headers.list.items[header_inx].value; + // (shoud this be case insensitive?) + if (!std.mem.startsWith(u8, request_header_value, header_value_needle)) return false; + // header value matches...return the path prefix match + const rc = std.mem.startsWith(u8, requested_path, path_needle); + if (rc) log.debug("executor match for header and path prefix '{s}'", .{match_data}); + return rc; +} + /// Loads all optional symbols from the dynamic library. This has two entry /// points, though the logic around the primary request handler is slighly /// different in each case so we can't combine those two. @@ -346,7 +380,7 @@ fn loadConfig(allocator: std.mem.Allocator) ![]Executor { defer al.deinit(); for (parsed_config.key_value_map.keys(), parsed_config.key_value_map.values()) |k, v| { al.appendAssumeCapacity(.{ - .target_prefix = k, + .match_data = k, .path = v, }); }