diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6f8e86..8305e29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,18 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: ./.github/actions/libextism + # - uses: ./.github/actions/libextism + - name: Install Extism CLI + shell: sh + run: sudo curl -s https://get.extism.org/cli | sh -s -- -q -y + + - name: Check Extism version + run: extism --version - name: Install Zig uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.13.0 - name: Check Zig Version run: zig version @@ -69,3 +77,5 @@ jobs: COUNT=$(echo $TEST | jq | grep "I'm the inner struct" | wc -l) test $COUNT -eq 3 + TEST=$(extism call zig-out/bin/basic-example.wasm http_headers --input '' --allow-host github.com --enable-http-response-headers --log-level debug 2>&1) + echo $TEST | grep "text/html" \ No newline at end of file diff --git a/examples/basic.zig b/examples/basic.zig index ec1ab63..325b2d2 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -163,6 +163,35 @@ export fn http_get() i32 { return 0; } +export fn http_headers() i32 { + const plugin = Plugin.init(allocator); + + var req = http.HttpRequest.init("GET", "https://github.com"); + defer req.deinit(allocator); + + const res = plugin.request(req, null) catch unreachable; + defer res.deinit(); + + if (res.status != 200) { + plugin.setError("request failed"); + return @as(i32, res.status); + } + var headers = res.headers(plugin.allocator) catch |err| { + plugin.setErrorFmt("err: {any}, failed to get headers from response!", .{err}) catch unreachable; + return -1; + }; + defer headers.deinit(); + + const content_type = headers.get("content-type"); + if (content_type) |t| { + plugin.logFmt(.Debug, "got content-type: {s}", .{t.value}) catch unreachable; + } else { + return 1; + } + + return 0; +} + export fn greet() i32 { const plugin = Plugin.init(allocator); const user = plugin.getConfig("user") catch unreachable orelse { diff --git a/src/ffi.zig b/src/ffi.zig index 815631a..691fe5d 100644 --- a/src/ffi.zig +++ b/src/ffi.zig @@ -17,6 +17,7 @@ pub extern "extism:host/env" fn store_u64(ExtismPointer, u64) void; pub extern "extism:host/env" fn load_u64(ExtismPointer) u64; pub extern "extism:host/env" fn http_request(ExtismPointer, ExtismPointer) ExtismPointer; pub extern "extism:host/env" fn http_status_code() i32; +pub extern "extism:host/env" fn http_headers() ExtismPointer; pub extern "extism:host/env" fn get_log_level() i32; pub extern "extism:host/env" fn log_trace(ExtismPointer) void; pub extern "extism:host/env" fn log_debug(ExtismPointer) void; diff --git a/src/http.zig b/src/http.zig index 9525dd7..ac59a27 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1,9 +1,51 @@ const std = @import("std"); const Memory = @import("Memory.zig"); +const extism = @import("ffi.zig"); + +pub const Headers = struct { + allocator: std.mem.Allocator, + raw: []const u8, + internal: std.json.ArrayHashMap([]const u8), + + /// Get a value (if it exists) from the Headers map at the provided name. + /// NOTE: this may be a multi-value header, and will be a comma-separated list. + pub fn get(self: Headers, name: []const u8) ?std.http.Header { + const val = self.internal.map.get(name); + if (val) |v| { + return std.http.Header{ + .name = name, + .value = v, + }; + } else { + return null; + } + } + + /// Access the internal data to iterate over or mutate as needed. + pub fn internal(self: Headers) std.json.ArrayHashMap([]const u8) { + return self.internal; + } + + /// Check if the Headers is empty. + pub fn isEmpty(self: Headers) bool { + return self.internal.map.entries.len == 0; + } + + /// Check if a header exists in the Headers. + pub fn contains(self: Headers, key: []const u8) bool { + return self.internal.map.contains(key); + } + + pub fn deinit(self: *Headers) void { + self.allocator.free(self.raw); + self.internal.deinit(self.allocator); + } +}; pub const HttpResponse = struct { memory: Memory, status: u16, + responseHeaders: Memory, /// IMPORTANT: it's the caller's responsibility to free the returned string pub fn body(self: HttpResponse, allocator: std.mem.Allocator) ![]u8 { @@ -15,11 +57,27 @@ pub const HttpResponse = struct { pub fn deinit(self: HttpResponse) void { self.memory.free(); + self.responseHeaders.free(); } pub fn statusCode(self: HttpResponse) u16 { return self.status; } + + /// IMPORTANT: it's the caller's responsibility to `deinit` the Headers if returned. + pub fn headers(self: HttpResponse, allocator: std.mem.Allocator) !Headers { + const data = try self.responseHeaders.loadAlloc(allocator); + errdefer allocator.free(data); + + const j = try std.json.parseFromSlice(std.json.ArrayHashMap([]const u8), allocator, data, .{ .allocate = .alloc_always, .ignore_unknown_fields = true }); + defer j.deinit(); + + return Headers{ + .allocator = allocator, + .raw = data, + .internal = j.value, + }; + } }; pub const HttpRequest = struct { diff --git a/src/main.zig b/src/main.zig index e56c2ff..0e7c430 100644 --- a/src/main.zig +++ b/src/main.zig @@ -143,6 +143,7 @@ pub const Plugin = struct { pub fn logMemory(self: Plugin, level: LogLevel, memory: Memory) void { _ = self; // to make the interface consistent + switch (level) { .Trace => extism.log_trace(memory.offset), .Debug => extism.log_debug(memory.offset), @@ -224,10 +225,15 @@ pub const Plugin = struct { const length = extism.length_unsafe(offset); const status: u16 = @intCast(extism.http_status_code()); + const headersOffset = extism.http_headers(); + const headersLength = extism.length_unsafe(headersOffset); + const headersMem = Memory.init(headersOffset, headersLength); + const mem = Memory.init(offset, length); return http.HttpResponse{ .memory = mem, .status = status, + .responseHeaders = headersMem, }; } };