Skip to content

Commit 3e6d4fe

Browse files
committed
JNI based Java filter
Signed-off-by: Takeshi Yoneda <tyoneda@netflix.com>
1 parent 1b67f16 commit 3e6d4fe

16 files changed

Lines changed: 876 additions & 7 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ bazel-*
66

77
access_logs/
88

9-
*.so
9+
*.so
10+
11+
*.jar

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ test-rust: ## Run the unit tests for the Rust codebase.
8282
@$(call print_success,Rust unit tests completed)
8383

8484
.PHONY: build
85-
build: build-go build-rust ## Build all dynamic modules.
85+
build: build-go build-rust build-java ## Build all dynamic modules.
8686

8787
.PHONY: build-go
8888
build-go: ## Build the Go dynamic module.
@@ -92,6 +92,14 @@ build-go: ## Build the Go dynamic module.
9292
@$(call print_task,Copying Go dynamic module for easier use with Envoy)
9393
@cp go/libgo_module.so integration/libgo_module.so
9494

95+
.PHONY: build-java
96+
build-java: ## Build the Java filter JAR and copy it to the integration directory.
97+
@$(call print_task,Building Java filter JAR)
98+
@make -C java
99+
@$(call print_success,Java filter JAR built at java/out/envoy-java-filter.jar)
100+
@$(call print_task,Copying Java filter JAR for use with Envoy)
101+
@cp java/out/envoy-java-filter.jar integration/envoy-java-filter.jar
102+
95103
.PHONY: build-rust
96104
build-rust: ## Build the Rust dynamic module.
97105
@$(call print_task,Building Rust dynamic module)
@@ -102,7 +110,7 @@ build-rust: ## Build the Rust dynamic module.
102110
@cp rust/target/debug/librust_module.so integration/librust_module.so || true
103111

104112
.PHONY: integration-test
105-
integration-test: build-go build-rust ## Run the integration tests.
113+
integration-test: build-go build-rust build-java ## Run the integration tests.
106114
@$(call print_task,Running integration tests)
107115
@cd integration && go test -v ./...
108116
@$(call print_success,Integration tests completed)

integration/envoy.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,44 @@ static_resources:
207207
typed_config:
208208
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
209209

210+
- address:
211+
socket_address:
212+
address: 0.0.0.0
213+
port_value: 1065
214+
filter_chains:
215+
- filters:
216+
- name: envoy.filters.network.http_connection_manager
217+
typed_config:
218+
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
219+
stat_prefix: ingress_http
220+
route_config:
221+
virtual_hosts:
222+
- name: local_route
223+
domains:
224+
- "*"
225+
routes:
226+
- match:
227+
prefix: "/"
228+
route:
229+
cluster: httpbin
230+
http_filters:
231+
- name: dynamic_modules/java_filter
232+
typed_config:
233+
"@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter
234+
dynamic_module_config:
235+
name: rust_module
236+
filter_name: java_filter
237+
filter_config:
238+
"@type": "type.googleapis.com/google.protobuf.StringValue"
239+
value: |
240+
{
241+
"jar_path": "./envoy-java-filter.jar",
242+
"class_name": "io.envoyproxy.dynamicmodules.ExampleFilter"
243+
}
244+
- name: envoy.filters.http.router
245+
typed_config:
246+
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
247+
210248
clusters:
211249
- name: httpbin
212250
# This demonstrates how to use the dynamic module HTTP filter as an upstream filter.

integration/main_test.go

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import (
66
"context"
77
"encoding/json"
88
"io"
9+
"net"
910
"net/http"
1011
"os"
1112
"os/exec"
13+
"path/filepath"
1214
"strconv"
1315
"strings"
1416
"testing"
@@ -26,12 +28,15 @@ func TestIntegration(t *testing.T) {
2628

2729
// Setup the httpbin upstream local server.
2830
httpbinHandler := httpbin.New()
29-
server := &http.Server{Addr: ":1234", Handler: httpbinHandler,
31+
server := &http.Server{Handler: httpbinHandler,
3032
ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 5 * time.Second,
3133
WriteTimeout: 5 * time.Second,
3234
}
35+
// Use tcp4 to avoid conflicting with any IPv6-only service already on :1234.
36+
httpbinListener, err := net.Listen("tcp4", ":1234")
37+
require.NoError(t, err)
3338
go func() {
34-
if err = server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
39+
if err := server.Serve(httpbinListener); err != nil && err != http.ErrServerClosed {
3540
t.Logf("HTTP server error: %v", err)
3641
}
3742
}()
@@ -43,7 +48,7 @@ func TestIntegration(t *testing.T) {
4348

4449
// Health check to ensure the server is up before starting tests.
4550
require.Eventually(t, func() bool {
46-
resp, err := http.Get("http://localhost:1234/uuid")
51+
resp, err := http.Get("http://127.0.0.1:1234/uuid")
4752
if err != nil {
4853
t.Logf("httpbin server not ready yet: %v", err)
4954
return false
@@ -60,6 +65,11 @@ func TestIntegration(t *testing.T) {
6065
require.NoError(t, os.Mkdir(accessLogsDir, 0o700))
6166
require.NoError(t, os.Chmod(accessLogsDir, 0o777))
6267

68+
// Detect the JVM server library directory so librust_module.so can dlopen
69+
// libjvm.so when the java_filter initialises the embedded JVM.
70+
jvmLibPath := jvmServerLibPath(t)
71+
t.Logf("JVM lib path: %s", jvmLibPath)
72+
6373
if envoyImage := cmp.Or(os.Getenv("ENVOY_IMAGE")); envoyImage != "" {
6474
cmd := exec.Command(
6575
"docker",
@@ -92,9 +102,15 @@ func TestIntegration(t *testing.T) {
92102

93103
cmd.Stdout = os.Stdout
94104
cmd.Stderr = os.Stderr
105+
existingLD := os.Getenv("LD_LIBRARY_PATH")
106+
ldLibPath := jvmLibPath
107+
if existingLD != "" {
108+
ldLibPath = jvmLibPath + ":" + existingLD
109+
}
95110
cmd.Env = append(os.Environ(),
96111
"ENVOY_DYNAMIC_MODULES_SEARCH_PATH="+cwd,
97112
"GODEBUG=cgocheck=0",
113+
"LD_LIBRARY_PATH="+ldLibPath,
98114
)
99115
require.NoError(t, cmd.Start())
100116
defer func() {
@@ -422,6 +438,47 @@ func TestIntegration(t *testing.T) {
422438
}, 30*time.Second, 200*time.Millisecond)
423439
})
424440

441+
t.Run("java_filter", func(t *testing.T) {
442+
require.Eventually(t, func() bool {
443+
req, err := http.NewRequest("GET", "http://127.0.0.1:1065/headers", nil)
444+
require.NoError(t, err)
445+
446+
resp, err := http.DefaultClient.Do(req)
447+
if err != nil {
448+
t.Logf("Envoy not ready yet: %v", err)
449+
return false
450+
}
451+
defer func() {
452+
require.NoError(t, resp.Body.Close())
453+
}()
454+
body, err := io.ReadAll(resp.Body)
455+
if err != nil {
456+
t.Logf("Envoy not ready yet: %v", err)
457+
return false
458+
}
459+
460+
t.Logf("response: headers=%v, body=%s", resp.Header, string(body))
461+
require.Equal(t, 200, resp.StatusCode)
462+
463+
// httpbin echoes request headers back as JSON.
464+
type httpBinHeadersBody struct {
465+
Headers map[string][]string `json:"headers"`
466+
}
467+
var headersBody httpBinHeadersBody
468+
require.NoError(t, json.Unmarshal(body, &headersBody))
469+
470+
// ExampleFilter adds x-java-filter: active to every request.
471+
require.Contains(t, headersBody.Headers["X-Java-Filter"], "active",
472+
"x-java-filter request header should be set by the Java filter")
473+
474+
// ExampleFilter mirrors the :path back as x-java-filter-path on the response.
475+
require.Equal(t, "/headers", resp.Header.Get("x-java-filter-path"),
476+
"x-java-filter-path response header should mirror the request :path")
477+
478+
return true
479+
}, 30*time.Second, 200*time.Millisecond)
480+
})
481+
425482
t.Run("http_metrics", func(t *testing.T) {
426483
// Send test request
427484
require.Eventually(t, func() bool {
@@ -495,3 +552,30 @@ func TestIntegration(t *testing.T) {
495552
}, 5*time.Second, 200*time.Millisecond)
496553
})
497554
}
555+
556+
// jvmServerLibPath returns the directory containing libjvm.so, needed by the
557+
// java_filter at runtime so the Rust module can dlopen the JVM.
558+
// It first checks $JAVA_HOME, then falls back to running `java -XshowSettings:all -version`.
559+
func jvmServerLibPath(t *testing.T) string {
560+
t.Helper()
561+
javaHome := os.Getenv("JAVA_HOME")
562+
if javaHome == "" {
563+
out, err := exec.Command("java", "-XshowSettings:all", "-version").CombinedOutput()
564+
if err == nil {
565+
for _, line := range strings.Split(string(out), "\n") {
566+
if strings.Contains(line, "java.home") {
567+
parts := strings.SplitN(line, "=", 2)
568+
if len(parts) == 2 {
569+
javaHome = strings.TrimSpace(parts[1])
570+
break
571+
}
572+
}
573+
}
574+
}
575+
}
576+
if javaHome == "" {
577+
t.Log("JAVA_HOME not set and could not be detected; java_filter may fail to load libjvm.so")
578+
return ""
579+
}
580+
return filepath.Join(javaHome, "lib", "server")
581+
}

java/Makefile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
JAVA_SRC_DIR := src
2+
JAVA_OUT_DIR := out
3+
JAR := $(JAVA_OUT_DIR)/envoy-java-filter.jar
4+
5+
JAVA_SOURCES := \
6+
$(JAVA_SRC_DIR)/io/envoyproxy/dynamicmodules/HeaderMutation.java \
7+
$(JAVA_SRC_DIR)/io/envoyproxy/dynamicmodules/EnvoyHttpFilter.java \
8+
$(JAVA_SRC_DIR)/io/envoyproxy/dynamicmodules/ExampleFilter.java
9+
10+
.PHONY: all clean
11+
12+
all: $(JAR)
13+
14+
$(JAR): $(JAVA_SOURCES)
15+
mkdir -p $(JAVA_OUT_DIR)
16+
javac -source 11 -target 11 -d $(JAVA_OUT_DIR) $(JAVA_SOURCES)
17+
jar cf $(JAR) -C $(JAVA_OUT_DIR) .
18+
@echo "Built $(JAR)"
19+
20+
clean:
21+
rm -rf $(JAVA_OUT_DIR)

java/out/envoy-java-filter.jar

2.25 KB
Binary file not shown.
283 Bytes
Binary file not shown.
1002 Bytes
Binary file not shown.
383 Bytes
Binary file not shown.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.envoyproxy.dynamicmodules;
2+
3+
/**
4+
* Interface for implementing Envoy HTTP filters in Java.
5+
*
6+
* <p>Implement this interface, package your class in a JAR (together with
7+
* {@link HeaderMutation}), and configure the Rust dynamic module:
8+
*
9+
* <pre>{@code
10+
* {
11+
* "jar_path": "/path/to/your-filter.jar",
12+
* "class_name": "com.example.YourFilter"
13+
* }
14+
* }</pre>
15+
*
16+
* <p>Your class must have a public no-arg constructor. A single instance is
17+
* created per filter-chain config block and reused across all requests, so
18+
* implementations must be thread-safe if Envoy dispatches requests concurrently.
19+
*/
20+
public interface EnvoyHttpFilter {
21+
22+
/**
23+
* Called when request headers arrive from the downstream client.
24+
*
25+
* @param names header names (parallel array)
26+
* @param values header values (parallel array, same length as {@code names})
27+
* @return mutations to apply, or {@code null} to leave headers unchanged
28+
* and continue the filter chain
29+
*/
30+
HeaderMutation onRequestHeaders(String[] names, String[] values);
31+
32+
/**
33+
* Called when response headers arrive from the upstream.
34+
*
35+
* @param names header names (parallel array)
36+
* @param values header values (parallel array, same length as {@code names})
37+
* @return mutations to apply, or {@code null} to leave headers unchanged
38+
* and continue the filter chain
39+
*/
40+
HeaderMutation onResponseHeaders(String[] names, String[] values);
41+
}

0 commit comments

Comments
 (0)