Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: add http host functions #25

Merged
merged 2 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
<maven-gpg-plugin.version>3.2.7</maven-gpg-plugin.version>
<jimfs.version>1.3.0</jimfs.version>
<jakarta.json-api.version>2.1.3</jakarta.json-api.version>
<jakarta.json.version>1.1.7</jakarta.json.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -102,7 +104,18 @@
<version>${jimfs.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>${jakarta.json-api.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.parsson</groupId>
<artifactId>jakarta.json</artifactId>
<version>${jakarta.json.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
1 change: 0 additions & 1 deletion src/main/java/org/extism/chicory/sdk/ChicoryModule.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.extism.chicory.sdk;

import com.dylibso.chicory.experimental.aot.AotMachine;
import com.dylibso.chicory.runtime.Instance;
import com.dylibso.chicory.wasm.Parser;
import com.dylibso.chicory.wasm.WasmModule;
Expand Down
204 changes: 195 additions & 9 deletions src/main/java/org/extism/chicory/sdk/HostEnv.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
import com.dylibso.chicory.log.Logger;
import com.dylibso.chicory.runtime.HostFunction;
import com.dylibso.chicory.runtime.Instance;
import com.dylibso.chicory.wasm.types.Value;
import com.dylibso.chicory.wasm.types.ValueType;

import jakarta.json.Json;
import jakarta.json.JsonObject;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.dylibso.chicory.wasm.types.Value.i64;

public class HostEnv {

private final Kernel kernel;
Expand All @@ -22,14 +28,16 @@ public class HostEnv {
private final Log log;
private final Var var;
private final Config config;
private final Http http;

public HostEnv(Kernel kernel, Map<String, String> config, Logger logger) {
public HostEnv(Kernel kernel, Map<String, String> config, String[] allowedHosts, Logger logger) {
this.kernel = kernel;
this.memory = new Memory();
this.logger = logger;
this.config = new Config(config);
this.var = new Var();
this.log = new Log();
this.http = new Http(allowedHosts);
}

public Log log() {
Expand All @@ -44,12 +52,17 @@ public Config config() {
return config;
}

public Http http() {
return http;
}

public HostFunction[] toHostFunctions() {
return concat(
kernel.toHostFunctions(),
log.toHostFunctions(),
var.toHostFunctions(),
config.toHostFunctions());
config.toHostFunctions(),
http.toHostFunctions());
}

private HostFunction[] concat(HostFunction[]... hfs) {
Expand Down Expand Up @@ -108,7 +121,8 @@ long writeString(String s) {
}

public class Log {
private Log(){}
private Log() {
}

public void log(LogLevel level, String message) {
logger.log(level.toChicoryLogLevel(), message, null);
Expand Down Expand Up @@ -156,9 +170,10 @@ HostFunction[] toHostFunctions() {
}

public class Var {
private final Map<String, byte[]> vars = new ConcurrentHashMap<>();
private final Map<String, byte[]> vars = new ConcurrentHashMap<>();

private Var() {}
private Var() {
}

public byte[] get(String key) {
return vars.get(key);
Expand Down Expand Up @@ -246,7 +261,178 @@ HostFunction[] toHostFunctions() {

}

public class Http {
private final HostPattern[] hostPatterns;
HttpClient httpClient;
HttpResponse<byte[]> lastResponse;

public Http(String[] allowedHosts) {
if (allowedHosts == null) {
allowedHosts = new String[0];
}
this.hostPatterns = new HostPattern[allowedHosts.length];
for (int i = 0; i < allowedHosts.length; i++) {
this.hostPatterns[i] = new HostPattern(allowedHosts[i]);
}
}

public HttpClient httpClient() {
if (httpClient == null) {
httpClient = HttpClient.newHttpClient();
}
return httpClient;
}

long[] request(Instance instance, long... args) {
var result = new long[1];

var requestOffset = args[0];
var bodyOffset = args[1];

var requestJson = memory().readBytes(requestOffset);
kernel.free.apply(requestOffset);

byte[] requestBody;
if (bodyOffset == 0) {
requestBody = new byte[0];
} else {
requestBody = memory().readBytes(bodyOffset);
kernel.free.apply(bodyOffset);
}

var request = Json.createReader(new ByteArrayInputStream(requestJson))
.readObject();

var method = request.getJsonString("method").getString();
var uri = URI.create(request.getJsonString("url").getString());
var headers = request.getJsonObject("headers");

Map<String, String> headersMap = new HashMap<>();
for (var key : headers.keySet()) {
headersMap.put(key, headers.getString(key));
}

byte[] body = request(method, uri, headersMap, requestBody);
if (body.length == 0) {
result[0] = 0;
} else {
result[0] = memory().writeBytes(body);
}

return result;
}

byte[] request(String method, URI uri, Map<String, String> headers, byte[] requestBody) {
HttpRequest.BodyPublisher bodyPublisher;
if (requestBody.length == 0) {
bodyPublisher = HttpRequest.BodyPublishers.noBody();
} else {
bodyPublisher = HttpRequest.BodyPublishers.ofByteArray(requestBody);
}

var host = uri.getHost();
if (Arrays.stream(hostPatterns).noneMatch(p -> p.matches(host))) {
throw new ExtismException(String.format("HTTP request to '%s' is not allowed", host));
}

var reqBuilder = HttpRequest.newBuilder().uri(uri);
for (var key : headers.keySet()) {
reqBuilder.header(key, headers.get(key));
}

var req = reqBuilder.method(method, bodyPublisher).build();

try {
this.lastResponse =
httpClient().send(req, HttpResponse.BodyHandlers.ofByteArray());
return lastResponse.body();
} catch (IOException | InterruptedException e) {
// FIXME gracefully handle the interruption
throw new ExtismException(e);
}
}

long[] statusCode(Instance instance, long... args) {
return new long[]{statusCode()};
}

int statusCode() {
return lastResponse == null ? 0 : lastResponse.statusCode();
}

long[] headers(Instance instance, long[] longs) {
var result = new long[1];
if (lastResponse == null) {
return result;
}

// FIXME duplicated headers are effectively overwriting duplicate values!
var objBuilder = Json.createObjectBuilder();
for (var entry : lastResponse.headers().map().entrySet()) {
for (var v : entry.getValue()) {
objBuilder.add(entry.getKey(), v);
}
}

var bytes = objBuilder.build().toString().getBytes(StandardCharsets.UTF_8);
result[0] = memory().writeBytes(bytes);
return result;
}


public HostFunction[] toHostFunctions() {
return new HostFunction[]{
new HostFunction(
Kernel.IMPORT_MODULE_NAME,
"http_request",
List.of(ValueType.I64, ValueType.I64),
List.of(ValueType.I64),
this::request),
new HostFunction(
Kernel.IMPORT_MODULE_NAME,
"http_status_code",
List.of(),
List.of(ValueType.I32),
this::statusCode),
new HostFunction(
Kernel.IMPORT_MODULE_NAME,
"http_headers",
List.of(),
List.of(ValueType.I64),
this::headers),

};
}
}

private static class HostPattern {
private final String pattern;
private final boolean exact;

public HostPattern(String pattern) {
if (pattern.indexOf('*', 1) != -1) {
throw new ExtismException("Illegal pattern " + pattern);
}
int wildcard = pattern.indexOf('*');
if (wildcard < 0) {
this.exact = true;
this.pattern = pattern;
} else if (wildcard == 0) {
this.exact = false;
this.pattern = pattern.substring(1);
} else {
throw new ExtismException("Illegal pattern " + pattern);
}
}

public boolean matches(String host) {
if (exact) {
return host.equals(pattern);
} else {
return host.endsWith(pattern);
}
}
}


}
5 changes: 4 additions & 1 deletion src/main/java/org/extism/chicory/sdk/Linker.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,24 @@ Plugin link() {
var dg = new DependencyGraph(logger);

Map<String, String> config;
String[] allowedHosts;
WasiOptions wasiOptions;
CachedAotMachineFactory aotMachineFactory;
if (manifest.options == null) {
config = Map.of();
allowedHosts = new String[0];
wasiOptions = null;
aotMachineFactory = null;
} else {
dg.setOptions(manifest.options);
config = manifest.options.config;
allowedHosts = manifest.options.allowedHosts;
wasiOptions = manifest.options.wasiOptions;
aotMachineFactory = manifest.options.aot? new CachedAotMachineFactory() : null;
}

// Register the HostEnv exports.
var hostEnv = new HostEnv(new Kernel(aotMachineFactory), config, logger);
var hostEnv = new HostEnv(new Kernel(aotMachineFactory), config, allowedHosts, logger);
dg.registerFunctions(hostEnv.toHostFunctions());

// Register the WASI host functions.
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/extism/chicory/sdk/Manifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.dylibso.chicory.wasi.WasiOptions;

import java.nio.file.Path;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
Expand All @@ -17,6 +18,7 @@ public static class Options {
EnumSet<Validation> validationFlags = EnumSet.noneOf(Validation.class);
Map<String, String> config;
WasiOptions wasiOptions;
String[] allowedHosts;

public Options withAoT() {
this.aot = true;
Expand All @@ -33,6 +35,17 @@ public Options withValidation(Validation... vs) {
return this;
}

public Options withAllowedHosts(String... allowedHosts) {
for (String allowedHost : allowedHosts) {
// Wildcards are only allowed at starting position and may occur only once.
if (allowedHost.indexOf('*') > 0 || allowedHost.indexOf('*', 1) != -1) {
throw new ExtismException("Illegal pattern " + allowedHost);
}
}
this.allowedHosts = allowedHosts;
return this;
}

public Options withWasi(WasiOptions wasiOptions) {
this.wasiOptions = wasiOptions;
return this;
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/extism/chicory/sdk/HostEnvTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void testShowcase() {
var logger = new SystemLogger();

var config = Map.of("key", "value");
var hostEnv = new HostEnv(new Kernel(), config, logger);
var hostEnv = new HostEnv(new Kernel(), config, new String[0], logger);

assertEquals(hostEnv.config().get("key"), "value");

Expand Down
Loading
Loading