From a86059f801001818ef99425c5f9e28efa3b34c45 Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Tue, 24 Mar 2026 19:03:58 +0100 Subject: [PATCH] Add support for X-Forwarded-* and RFC 7239 Forwarded headers Closes #130 Configure forwarded headers via ForwardedHeadersOptions to send client request information to origin servers. Supports both de-facto X-Forwarded-* headers (For, Proto, Host, Port) and standardized RFC 7239 Forwarded header. Disabled by default for backwards compatibility. Some portions of this content were created with the assistance of Claude Code. Signed-off-by: Thomas Segismont --- src/main/asciidoc/index.adoc | 39 +++ .../ForwardedHeadersOptionsConverter.java | 61 ++++ .../httpproxy/ProxyOptionsConverter.java | 8 + src/main/java/examples/HttpProxyExamples.java | 43 +++ .../httpproxy/ForwardedHeadersOptions.java | 234 +++++++++++++ .../java/io/vertx/httpproxy/ProxyOptions.java | 34 ++ .../java/io/vertx/httpproxy/ProxyRequest.java | 9 +- .../vertx/httpproxy/impl/ProxiedRequest.java | 183 +++++++++- .../io/vertx/httpproxy/impl/ReverseProxy.java | 10 +- .../io/vertx/tests/ForwardedHeadersTest.java | 321 ++++++++++++++++++ src/test/resources/SSL_TEST_CERTIFICATES.txt | 49 +++ src/test/resources/server.cert.pem | 19 ++ src/test/resources/server.key.pem | 28 ++ 13 files changed, 1029 insertions(+), 9 deletions(-) create mode 100644 src/main/generated/io/vertx/httpproxy/ForwardedHeadersOptionsConverter.java create mode 100644 src/main/java/io/vertx/httpproxy/ForwardedHeadersOptions.java create mode 100644 src/test/java/io/vertx/tests/ForwardedHeadersTest.java create mode 100644 src/test/resources/SSL_TEST_CERTIFICATES.txt create mode 100644 src/test/resources/server.cert.pem create mode 100644 src/test/resources/server.key.pem diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index ffda0ff..9f82c27 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -84,6 +84,45 @@ Nevertheless, you can override the request authority: When the request authority is overridden, the `x-forwarded-host` header is automatically set on the request to the origin server with the original authority value. +==== Forwarded headers + +As a reverse proxy, it's often required to send information about the client request to the origin server through headers. +The proxy supports both the de-facto standard `X-Forwarded-*` headers and the RFC 7239 `Forwarded` header. + +By default, forwarded headers are disabled. +You can enable them by configuring {@link io.vertx.httpproxy.ForwardedHeadersOptions}: + +[source,java] +---- +{@link examples.HttpProxyExamples#forwardedHeaders} +---- + +You can control which headers are added: + +[source,java] +---- +{@link examples.HttpProxyExamples#forwardedHeadersSelective} +---- + +Alternatively, you can use the RFC 7239 standardized `Forwarded` header instead of the de-facto `X-Forwarded-*` headers: + +[source,java] +---- +{@link examples.HttpProxyExamples#forwardedHeadersRfc7239} +---- + +The following headers are supported: + +* `X-Forwarded-For` - contains the client IP address. +When a proxy chain exists, the proxy appends the client IP to preserve the chain. +* `X-Forwarded-Proto` - contains the original request scheme (http or https) +* `X-Forwarded-Host` - contains the original request host +* `X-Forwarded-Port` - contains the original request port +* `Forwarded` - RFC 7239 standardized header that combines the above information in a structured format: `for=;proto=;host=` + +IMPORTANT: The `X-Forwarded-*` and `Forwarded` headers can be used by origin servers to determine the actual client IP, protocol, and host when behind a reverse proxy. +This is useful for generating proper URLs, security decisions, and logging. + === WebSockets The proxy supports WebSocket by default. diff --git a/src/main/generated/io/vertx/httpproxy/ForwardedHeadersOptionsConverter.java b/src/main/generated/io/vertx/httpproxy/ForwardedHeadersOptionsConverter.java new file mode 100644 index 0000000..995ecd8 --- /dev/null +++ b/src/main/generated/io/vertx/httpproxy/ForwardedHeadersOptionsConverter.java @@ -0,0 +1,61 @@ +package io.vertx.httpproxy; + +import io.vertx.core.json.JsonObject; +import io.vertx.core.json.JsonArray; + +/** + * Converter and mapper for {@link io.vertx.httpproxy.ForwardedHeadersOptions}. + * NOTE: This class has been automatically generated from the {@link io.vertx.httpproxy.ForwardedHeadersOptions} original class using Vert.x codegen. + */ +public class ForwardedHeadersOptionsConverter { + + static void fromJson(Iterable> json, ForwardedHeadersOptions obj) { + for (java.util.Map.Entry member : json) { + switch (member.getKey()) { + case "enabled": + if (member.getValue() instanceof Boolean) { + obj.setEnabled((Boolean)member.getValue()); + } + break; + case "forwardFor": + if (member.getValue() instanceof Boolean) { + obj.setForwardFor((Boolean)member.getValue()); + } + break; + case "forwardProto": + if (member.getValue() instanceof Boolean) { + obj.setForwardProto((Boolean)member.getValue()); + } + break; + case "forwardHost": + if (member.getValue() instanceof Boolean) { + obj.setForwardHost((Boolean)member.getValue()); + } + break; + case "forwardPort": + if (member.getValue() instanceof Boolean) { + obj.setForwardPort((Boolean)member.getValue()); + } + break; + case "useRfc7239": + if (member.getValue() instanceof Boolean) { + obj.setUseRfc7239((Boolean)member.getValue()); + } + break; + } + } + } + + static void toJson(ForwardedHeadersOptions obj, JsonObject json) { + toJson(obj, json.getMap()); + } + + static void toJson(ForwardedHeadersOptions obj, java.util.Map json) { + json.put("enabled", obj.isEnabled()); + json.put("forwardFor", obj.isForwardFor()); + json.put("forwardProto", obj.isForwardProto()); + json.put("forwardHost", obj.isForwardHost()); + json.put("forwardPort", obj.isForwardPort()); + json.put("useRfc7239", obj.isUseRfc7239()); + } +} diff --git a/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java b/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java index 97f2ed7..33c57ea 100644 --- a/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java +++ b/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java @@ -22,6 +22,11 @@ static void fromJson(Iterable> json, ProxyOp obj.setSupportWebSocket((Boolean)member.getValue()); } break; + case "forwardedHeadersOptions": + if (member.getValue() instanceof JsonObject) { + obj.setForwardedHeadersOptions(new io.vertx.httpproxy.ForwardedHeadersOptions((io.vertx.core.json.JsonObject)member.getValue())); + } + break; } } } @@ -35,5 +40,8 @@ static void toJson(ProxyOptions obj, java.util.Map json) { json.put("cacheOptions", obj.getCacheOptions().toJson()); } json.put("supportWebSocket", obj.getSupportWebSocket()); + if (obj.getForwardedHeadersOptions() != null) { + json.put("forwardedHeadersOptions", obj.getForwardedHeadersOptions().toJson()); + } } } diff --git a/src/main/java/examples/HttpProxyExamples.java b/src/main/java/examples/HttpProxyExamples.java index 799bc78..a5f51c8 100644 --- a/src/main/java/examples/HttpProxyExamples.java +++ b/src/main/java/examples/HttpProxyExamples.java @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + package examples; import io.vertx.core.Future; @@ -214,4 +225,36 @@ public Future handleProxyRequest(ProxyContext context) { public void cacheConfig(Vertx vertx, HttpClient proxyClient) { HttpProxy proxy = HttpProxy.reverseProxy(new ProxyOptions().setCacheOptions(new CacheOptions()), proxyClient); } + + public void forwardedHeaders(Vertx vertx, HttpClient proxyClient) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true)); + + HttpProxy proxy = HttpProxy.reverseProxy(options, proxyClient); + proxy.origin(7070, "origin"); + } + + public void forwardedHeadersSelective(Vertx vertx, HttpClient proxyClient) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardFor(true) + .setForwardProto(true) + .setForwardHost(true) + .setForwardPort(false)); // Don't forward port + + HttpProxy proxy = HttpProxy.reverseProxy(options, proxyClient); + proxy.origin(7070, "origin"); + } + + public void forwardedHeadersRfc7239(Vertx vertx, HttpClient proxyClient) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setUseRfc7239(true)); // Use RFC 7239 Forwarded header + + HttpProxy proxy = HttpProxy.reverseProxy(options, proxyClient); + proxy.origin(7070, "origin"); + } } diff --git a/src/main/java/io/vertx/httpproxy/ForwardedHeadersOptions.java b/src/main/java/io/vertx/httpproxy/ForwardedHeadersOptions.java new file mode 100644 index 0000000..14d4dcb --- /dev/null +++ b/src/main/java/io/vertx/httpproxy/ForwardedHeadersOptions.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; +import io.vertx.core.json.JsonObject; + +/** + * Options for configuring forwarded headers support in the proxy. + *

+ * These headers are used to preserve information about the original client request when proxying through one or more intermediaries. + *

+ * The proxy can add either {@code X-Forwarded-*} headers (de-facto standard) or the RFC 7239 {@code Forwarded} header. + */ +@DataObject +@JsonGen(publicConverter = false) +public class ForwardedHeadersOptions { + + /** + * Default enabled = {@code false} + */ + public static final boolean DEFAULT_ENABLED = false; + + /** + * Default forward for = {@code true} + */ + public static final boolean DEFAULT_FORWARD_FOR = true; + + /** + * Default forward proto = {@code true} + */ + public static final boolean DEFAULT_FORWARD_PROTO = true; + + /** + * Default forward host = {@code true} + */ + public static final boolean DEFAULT_FORWARD_HOST = true; + + /** + * Default forward port = {@code true} + */ + public static final boolean DEFAULT_FORWARD_PORT = true; + + /** + * Default use RFC 7239 = {@code false} + */ + public static final boolean DEFAULT_USE_RFC7239 = false; + + private boolean enabled = DEFAULT_ENABLED; + private boolean forwardFor = DEFAULT_FORWARD_FOR; + private boolean forwardProto = DEFAULT_FORWARD_PROTO; + private boolean forwardHost = DEFAULT_FORWARD_HOST; + private boolean forwardPort = DEFAULT_FORWARD_PORT; + private boolean useRfc7239 = DEFAULT_USE_RFC7239; + + /** + * Default constructor. + */ + public ForwardedHeadersOptions() { + } + + /** + * Copy constructor. + * + * @param other the options to copy + */ + public ForwardedHeadersOptions(ForwardedHeadersOptions other) { + this.enabled = other.isEnabled(); + this.forwardFor = other.isForwardFor(); + this.forwardProto = other.isForwardProto(); + this.forwardHost = other.isForwardHost(); + this.forwardPort = other.isForwardPort(); + this.useRfc7239 = other.isUseRfc7239(); + } + + /** + * Constructor to create an options from JSON. + * + * @param json the JSON + */ + public ForwardedHeadersOptions(JsonObject json) { + ForwardedHeadersOptionsConverter.fromJson(json, this); + } + + /** + * @return whether forwarded headers support is enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Set whether forwarded headers support is enabled. + *

+ * When disabled, no forwarded headers will be added to proxied requests. + * + * @param enabled {@code true} to enable forwarded headers + * @return a reference to this, so the API can be used fluently + */ + public ForwardedHeadersOptions setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * @return whether to forward client IP address + */ + public boolean isForwardFor() { + return forwardFor; + } + + /** + * Set whether to forward the client IP address. + *

+ * When enabled, adds the {@code X-Forwarded-For} header (or the 'for' parameter in RFC 7239 Forwarded header) with the client's IP address. + * If the header already exists, the client IP is appended to preserve the proxy chain. + * + * @param forwardFor {@code true} to forward client IP + * @return a reference to this, so the API can be used fluently + */ + public ForwardedHeadersOptions setForwardFor(boolean forwardFor) { + this.forwardFor = forwardFor; + return this; + } + + /** + * @return whether to forward the original protocol (http/https) + */ + public boolean isForwardProto() { + return forwardProto; + } + + /** + * Set whether to forward the original protocol (http/https). + *

+ * When enabled, adds the {@code X-Forwarded-Proto} header (or the 'proto' parameter in RFC 7239 Forwarded header) with the original request scheme. + * + * @param forwardProto {@code true} to forward protocol + * @return a reference to this, so the API can be used fluently + */ + public ForwardedHeadersOptions setForwardProto(boolean forwardProto) { + this.forwardProto = forwardProto; + return this; + } + + /** + * @return whether to forward the original host + */ + public boolean isForwardHost() { + return forwardHost; + } + + /** + * Set whether to forward the original host. + *

+ * When enabled, adds the {@code X-Forwarded-Host} header (or the 'host' parameter in RFC 7239 Forwarded header) with the original request host. + * This is only added if the host was not already set by {@link ProxyRequest#setAuthority(io.vertx.core.net.HostAndPort)}. + * + * @param forwardHost {@code true} to forward host + * @return a reference to this, so the API can be used fluently + */ + public ForwardedHeadersOptions setForwardHost(boolean forwardHost) { + this.forwardHost = forwardHost; + return this; + } + + /** + * @return whether to forward the original port + */ + public boolean isForwardPort() { + return forwardPort; + } + + /** + * Set whether to forward the original port. + *

+ * When enabled, adds the {@code X-Forwarded-Port} header with the original request port. + * This parameter is not included in RFC 7239 Forwarded header. + * + * @param forwardPort {@code true} to forward port + * @return a reference to this, so the API can be used fluently + */ + public ForwardedHeadersOptions setForwardPort(boolean forwardPort) { + this.forwardPort = forwardPort; + return this; + } + + /** + * @return whether to use RFC 7239 Forwarded header instead of {@code X-Forwarded-*} headers + */ + public boolean isUseRfc7239() { + return useRfc7239; + } + + /** + * Set whether to use RFC 7239 Forwarded header instead of {@code X-Forwarded-*} headers. + *

+ * When enabled, uses the standardized {@code Forwarded} header instead of the de-facto + * {@code X-Forwarded-For}, {@code X-Forwarded-Proto}, and {@code X-Forwarded-Host} headers. The {@code X-Forwarded-Port} + * header is not included in RFC 7239. + * + * @param useRfc7239 {@code true} to use RFC 7239 format + * @return a reference to this, so the API can be used fluently + */ + public ForwardedHeadersOptions setUseRfc7239(boolean useRfc7239) { + this.useRfc7239 = useRfc7239; + return this; + } + + @Override + public String toString() { + return toJson().toString(); + } + + /** + * Convert to JSON. + * + * @return the JSON + */ + public JsonObject toJson() { + JsonObject json = new JsonObject(); + ForwardedHeadersOptionsConverter.toJson(this, json); + return json; + } +} diff --git a/src/main/java/io/vertx/httpproxy/ProxyOptions.java b/src/main/java/io/vertx/httpproxy/ProxyOptions.java index bae6bed..654e21b 100644 --- a/src/main/java/io/vertx/httpproxy/ProxyOptions.java +++ b/src/main/java/io/vertx/httpproxy/ProxyOptions.java @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + package io.vertx.httpproxy; import io.vertx.codegen.annotations.DataObject; @@ -19,6 +30,7 @@ public class ProxyOptions { private CacheOptions cacheOptions; private boolean supportWebSocket; + private ForwardedHeadersOptions forwardedHeadersOptions; public ProxyOptions(JsonObject json) { ProxyOptionsConverter.fromJson(json, this); @@ -66,6 +78,28 @@ public ProxyOptions setSupportWebSocket(boolean supportWebSocket) { return this; } + /** + * @return the forwarded headers options + */ + public ForwardedHeadersOptions getForwardedHeadersOptions() { + return forwardedHeadersOptions; + } + + /** + * Set the forwarded headers options that configures how the proxy handles + * X-Forwarded-* or RFC 7239 Forwarded headers. + *

+ * {@code null} forwarded headers options disables forwarded headers support, + * by default forwarded headers support is disabled. + * + * @param forwardedHeadersOptions the forwarded headers options + * @return a reference to this, so the API can be used fluently + */ + public ProxyOptions setForwardedHeadersOptions(ForwardedHeadersOptions forwardedHeadersOptions) { + this.forwardedHeadersOptions = forwardedHeadersOptions; + return this; + } + @Override public String toString() { return toJson().toString(); diff --git a/src/main/java/io/vertx/httpproxy/ProxyRequest.java b/src/main/java/io/vertx/httpproxy/ProxyRequest.java index 30d96d5..9dbef2e 100644 --- a/src/main/java/io/vertx/httpproxy/ProxyRequest.java +++ b/src/main/java/io/vertx/httpproxy/ProxyRequest.java @@ -38,8 +38,15 @@ public interface ProxyRequest { * @return a reference to this, so the API can be used fluently */ static ProxyRequest reverseProxy(HttpServerRequest proxiedRequest) { + return reverseProxy(proxiedRequest, null); + } + + /** + * Like {@link #reverseProxy(HttpServerRequest)} but using specific For + */ + static ProxyRequest reverseProxy(HttpServerRequest proxiedRequest, ForwardedHeadersOptions forwardedHeadersOptions) { proxiedRequest.pause(); - return new ProxiedRequest(proxiedRequest); + return new ProxiedRequest(proxiedRequest, forwardedHeadersOptions); } /** diff --git a/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java b/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java index 83882f6..a949d35 100644 --- a/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java +++ b/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -17,8 +17,10 @@ import io.vertx.core.internal.ContextInternal; import io.vertx.core.internal.http.HttpServerRequestInternal; import io.vertx.core.net.HostAndPort; +import io.vertx.core.net.SocketAddress; import io.vertx.core.streams.Pipe; import io.vertx.httpproxy.Body; +import io.vertx.httpproxy.ForwardedHeadersOptions; import io.vertx.httpproxy.ProxyRequest; import io.vertx.httpproxy.ProxyResponse; @@ -30,6 +32,17 @@ public class ProxiedRequest implements ProxyRequest { private static final CharSequence X_FORWARDED_HOST = HttpHeaders.createOptimized("x-forwarded-host"); + private static final CharSequence X_FORWARDED_FOR = HttpHeaders.createOptimized("x-forwarded-for"); + private static final CharSequence X_FORWARDED_PROTO = HttpHeaders.createOptimized("x-forwarded-proto"); + private static final CharSequence X_FORWARDED_PORT = HttpHeaders.createOptimized("x-forwarded-port"); + private static final CharSequence FORWARDED = HttpHeaders.createOptimized("forwarded"); + + // Forwarded headers bit flags + private static final int FLAG_FORWARD_FOR = 1; // bit 0 + private static final int FLAG_FORWARD_PROTO = 2; // bit 1 + private static final int FLAG_FORWARD_HOST = 4; // bit 2 + private static final int FLAG_FORWARD_PORT = 8; // bit 3 + private static final int FLAG_USE_RFC7239 = 16; // bit 4 private static final MultiMap HOP_BY_HOP_HEADERS = MultiMap.caseInsensitiveMultiMap() .add(CONNECTION, "whatever") @@ -51,9 +64,9 @@ public class ProxiedRequest implements ProxyRequest { private final MultiMap headers; HttpClientRequest request; private final HttpServerRequest proxiedRequest; + private final int forwardedHeadersFlags; - public ProxiedRequest(HttpServerRequest proxiedRequest) { - + public ProxiedRequest(HttpServerRequest proxiedRequest, ForwardedHeadersOptions forwardedHeadersOptions) { // Determine content length long contentLength = -1L; String contentLengthHeader = proxiedRequest.getHeader(CONTENT_LENGTH); @@ -77,6 +90,32 @@ public ProxiedRequest(HttpServerRequest proxiedRequest) { this.proxiedRequest = proxiedRequest; this.context = ((HttpServerRequestInternal) proxiedRequest).context(); this.authority = null; // null is used as a signal to indicate an unchanged authority + + // Convert forwarded headers options to bit flags for efficient checking + this.forwardedHeadersFlags = buildForwardedHeadersFlags(forwardedHeadersOptions); + } + + private static int buildForwardedHeadersFlags(ForwardedHeadersOptions options) { + if (options == null || !options.isEnabled()) { + return 0; + } + int flags = 0; + if (options.isForwardFor()) { + flags |= FLAG_FORWARD_FOR; + } + if (options.isForwardProto()) { + flags |= FLAG_FORWARD_PROTO; + } + if (options.isForwardHost()) { + flags |= FLAG_FORWARD_HOST; + } + if (options.isForwardPort()) { + flags |= FLAG_FORWARD_PORT; + } + if (options.isUseRfc7239()) { + flags |= FLAG_USE_RFC7239; + } + return flags; } @Override @@ -179,6 +218,11 @@ Future sendRequest() { } } + // Add forwarded headers if configured + if (forwardedHeadersFlags != 0) { + addForwardedHeaders(request); + } + if (body == null) { if (proxiedRequest.headers().contains(CONTENT_LENGTH)) { request.putHeader(CONTENT_LENGTH, "0"); @@ -209,6 +253,139 @@ Future sendRequest() { }); } + private void addForwardedHeaders(HttpClientRequest request) { + if ((forwardedHeadersFlags & FLAG_USE_RFC7239) != 0) { + addRfc7239ForwardedHeader(request); + } else { + addXForwardedHeaders(request); + } + } + + private void addRfc7239ForwardedHeader(HttpClientRequest request) { + int capacity = estimateRfc7239Capacity(forwardedHeadersFlags); + StringBuilder forwarded = new StringBuilder(capacity); + if ((forwardedHeadersFlags & FLAG_FORWARD_FOR) != 0) { + appendRfc7239For(forwarded); + } + if ((forwardedHeadersFlags & FLAG_FORWARD_PROTO) != 0) { + appendRfc7239Proto(forwarded); + } + if ((forwardedHeadersFlags & FLAG_FORWARD_HOST) != 0) { + appendRfc7239Host(forwarded); + } + // If we reach here with non-zero flags, at least one component should be added + appendHeader(request, FORWARDED, forwarded.toString()); + } + + private static int estimateRfc7239Capacity(int flags) { + // Capacity estimates to avoid StringBuilder resizing: + // for=IPv6(39) + quotes(2) + prefix(4) ≈ 50 + // proto=https(5) + prefix(6) ≈ 15 + // host=domain(253) + port(6) + quotes(2) + prefix(5) ≈ 260 + int capacity = 0; + if ((flags & FLAG_FORWARD_FOR) != 0) { + capacity += 50; + } + if ((flags & FLAG_FORWARD_PROTO) != 0) { + capacity += 15; + } + if ((flags & FLAG_FORWARD_HOST) != 0) { + capacity += 260; + } + return capacity; + } + + private void appendRfc7239For(StringBuilder forwarded) { + String clientIp = getClientIp(); + if (clientIp != null) { + forwarded.append("for=").append(quoteIfNeeded(clientIp)); + } + } + + private void appendRfc7239Proto(StringBuilder forwarded) { + String proto = proxiedRequest.scheme(); + if (proto != null) { + if (forwarded.length() > 0) forwarded.append(";"); + forwarded.append("proto=").append(proto); + } + } + + private void appendRfc7239Host(StringBuilder forwarded) { + HostAndPort host = proxiedRequest.authority(); + if (host != null) { + if (forwarded.length() > 0) forwarded.append(";"); + forwarded.append("host=").append(quoteIfNeeded(host.toString())); + } + } + + private void addXForwardedHeaders(HttpClientRequest request) { + if ((forwardedHeadersFlags & FLAG_FORWARD_FOR) != 0) { + addXForwardedFor(request); + } + if ((forwardedHeadersFlags & FLAG_FORWARD_PROTO) != 0) { + addXForwardedProto(request); + } + if ((forwardedHeadersFlags & FLAG_FORWARD_HOST) != 0) { + addXForwardedHost(request); + } + if ((forwardedHeadersFlags & FLAG_FORWARD_PORT) != 0) { + addXForwardedPort(request); + } + } + + private void addXForwardedFor(HttpClientRequest request) { + String clientIp = getClientIp(); + if (clientIp != null) { + appendHeader(request, X_FORWARDED_FOR, clientIp); + } + } + + private void addXForwardedProto(HttpClientRequest request) { + String proto = proxiedRequest.scheme(); + if (proto != null) { + request.putHeader(X_FORWARDED_PROTO, proto); + } + } + + private void addXForwardedHost(HttpClientRequest request) { + // Only add if not already set by setAuthority() logic + if (!request.headers().contains(X_FORWARDED_HOST)) { + HostAndPort host = proxiedRequest.authority(); + if (host != null) { + request.putHeader(X_FORWARDED_HOST, host.host()); + } + } + } + + private void addXForwardedPort(HttpClientRequest request) { + HostAndPort host = proxiedRequest.authority(); + if (host != null) { + request.putHeader(X_FORWARDED_PORT, String.valueOf(host.port())); + } + } + + private String getClientIp() { + SocketAddress remoteAddress = proxiedRequest.remoteAddress(); + return remoteAddress != null ? remoteAddress.hostAddress() : null; + } + + private void appendHeader(HttpClientRequest request, CharSequence name, String value) { + String existing = request.headers().get(name); + if (existing != null) { + request.putHeader(name, existing + ", " + value); + } else { + request.putHeader(name, value); + } + } + + private String quoteIfNeeded(String value) { + // Quote IPv6 addresses and values containing special characters + if (value.contains(":") || value.contains(";") || value.contains(",")) { + return "\"" + value + "\""; + } + return value; + } + private static boolean equals(HostAndPort hp1, HostAndPort hp2) { if (hp1 == null || hp2 == null) { return false; diff --git a/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java b/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java index cda186f..27299ce 100644 --- a/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java +++ b/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -27,15 +27,14 @@ import java.util.*; -import static io.vertx.core.http.HttpHeaders.CONNECTION; -import static io.vertx.core.http.HttpHeaders.HOST; -import static io.vertx.core.http.HttpHeaders.UPGRADE; +import static io.vertx.core.http.HttpHeaders.*; public class ReverseProxy implements HttpProxy { private final static Logger log = LoggerFactory.getLogger(ReverseProxy.class); private final HttpClient client; private final boolean supportWebSocket; + private final ForwardedHeadersOptions forwardedHeadersOptions; private OriginRequestProvider originRequestProvider = (pc) -> Future.failedFuture("No origin available"); private final List interceptors = new ArrayList<>(); @@ -47,6 +46,7 @@ public ReverseProxy(ProxyOptions options, HttpClient client) { } this.client = client; this.supportWebSocket = options.getSupportWebSocket(); + this.forwardedHeadersOptions = options.getForwardedHeadersOptions(); } public Cache newCache(CacheOptions options, Vertx vertx) { @@ -73,7 +73,7 @@ public HttpProxy addInterceptor(ProxyInterceptor interceptor, boolean supportsWe @Override public void handle(HttpServerRequest request) { - ProxyRequest proxyRequest = ProxyRequest.reverseProxy(request); + ProxyRequest proxyRequest = ProxyRequest.reverseProxy(request, forwardedHeadersOptions); // Encoding sanity check Boolean chunked = HttpUtils.isChunked(request.headers()); diff --git a/src/test/java/io/vertx/tests/ForwardedHeadersTest.java b/src/test/java/io/vertx/tests/ForwardedHeadersTest.java new file mode 100644 index 0000000..4744888 --- /dev/null +++ b/src/test/java/io/vertx/tests/ForwardedHeadersTest.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ +package io.vertx.tests; + +import io.vertx.core.Handler; +import io.vertx.core.http.*; +import io.vertx.core.net.PemKeyCertOptions; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.httpproxy.ForwardedHeadersOptions; +import io.vertx.httpproxy.ProxyOptions; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.function.Consumer; + +@RunWith(VertxUnitRunner.class) +public class ForwardedHeadersTest extends TestBase { + + public ForwardedHeadersTest() { + super(new ProxyOptions()); + } + + @Test + public void testForwardedHeadersDisabledByDefault(TestContext ctx) { + runForwardedHeadersTest(ctx, null, null, null, req -> { + ctx.assertNull(req.getHeader("X-Forwarded-For")); + ctx.assertNull(req.getHeader("X-Forwarded-Proto")); + ctx.assertNull(req.getHeader("X-Forwarded-Host")); + ctx.assertNull(req.getHeader("X-Forwarded-Port")); + ctx.assertNull(req.getHeader("Forwarded")); + }); + } + + @Test + public void testXForwardedForEnabled(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardFor(true) + .setForwardProto(false) + .setForwardHost(false) + .setForwardPort(false) + ); + + runForwardedHeadersTest(ctx, options, null, null, req -> { + String xForwardedFor = req.getHeader("X-Forwarded-For"); + ctx.assertNotNull(xForwardedFor); + ctx.assertTrue(xForwardedFor.contains("127.0.0.1") || xForwardedFor.contains("0:0:0:0:0:0:0:1")); + ctx.assertNull(req.getHeader("X-Forwarded-Proto")); + ctx.assertNull(req.getHeader("X-Forwarded-Host")); + ctx.assertNull(req.getHeader("X-Forwarded-Port")); + }); + } + + @Test + public void testXForwardedProtoEnabled(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardFor(false) + .setForwardProto(true) + .setForwardHost(false) + .setForwardPort(false) + ); + + runForwardedHeadersTest(ctx, options, null, null, req -> { + String xForwardedProto = req.getHeader("X-Forwarded-Proto"); + ctx.assertNotNull(xForwardedProto); + ctx.assertEquals("http", xForwardedProto); + ctx.assertNull(req.getHeader("X-Forwarded-For")); + }); + } + + @Test + public void testXForwardedHostEnabled(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardFor(false) + .setForwardProto(false) + .setForwardHost(true) + .setForwardPort(false) + ); + + runForwardedHeadersTest(ctx, options, null, null, req -> { + String xForwardedHost = req.getHeader("X-Forwarded-Host"); + ctx.assertNotNull(xForwardedHost); + ctx.assertEquals("localhost", xForwardedHost); + }); + } + + @Test + public void testXForwardedPortEnabled(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardFor(false) + .setForwardProto(false) + .setForwardHost(false) + .setForwardPort(true) + ); + + runForwardedHeadersTest(ctx, options, null, null, req -> { + String xForwardedPort = req.getHeader("X-Forwarded-Port"); + ctx.assertNotNull(xForwardedPort); + ctx.assertEquals("8080", xForwardedPort); + }); + } + + @Test + public void testAllXForwardedHeadersEnabled(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardFor(true) + .setForwardProto(true) + .setForwardHost(true) + .setForwardPort(true) + ); + + runForwardedHeadersTest(ctx, options, null, null, req -> { + String xForwardedFor = req.getHeader("X-Forwarded-For"); + ctx.assertNotNull(xForwardedFor); + ctx.assertTrue(xForwardedFor.contains("127.0.0.1") || xForwardedFor.contains("0:0:0:0:0:0:0:1")); + + String xForwardedProto = req.getHeader("X-Forwarded-Proto"); + ctx.assertNotNull(xForwardedProto); + ctx.assertEquals("http", xForwardedProto); + + String xForwardedHost = req.getHeader("X-Forwarded-Host"); + ctx.assertNotNull(xForwardedHost); + ctx.assertEquals("localhost", xForwardedHost); + + String xForwardedPort = req.getHeader("X-Forwarded-Port"); + ctx.assertNotNull(xForwardedPort); + ctx.assertEquals("8080", xForwardedPort); + }); + } + + @Test + public void testXForwardedForChainPreservation(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardFor(true) + ); + + runForwardedHeadersTest(ctx, options, null, req -> { + req.putHeader("X-Forwarded-For", "192.168.1.100"); + }, req -> { + String xForwardedFor = req.getHeader("X-Forwarded-For"); + ctx.assertNotNull(xForwardedFor); + // Should contain both the original client and the proxy's append + List parts = List.of(xForwardedFor.split(",\\s*")); + ctx.assertTrue(parts.size() >= 2); + ctx.assertEquals("192.168.1.100", parts.get(0)); + ctx.assertTrue(parts.get(1).contains("127.0.0.1") || parts.get(1).contains("0:0:0:0:0:0:0:1")); + }); + } + + @Test + public void testRfc7239ForwardedHeader(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setUseRfc7239(true) + .setForwardFor(true) + .setForwardProto(true) + .setForwardHost(true) + ); + + // Should NOT have X-Forwarded-* headers when using RFC 7239 + runForwardedHeadersTest(ctx, options, null, null, req -> { + String forwarded = req.getHeader("Forwarded"); + ctx.assertNotNull(forwarded); + ctx.assertTrue(forwarded.contains("for=")); + ctx.assertTrue(forwarded.contains("proto=http")); + ctx.assertTrue(forwarded.contains("host=")); + + // Should NOT have X-Forwarded-* headers when using RFC 7239 + ctx.assertNull(req.getHeader("X-Forwarded-For")); + ctx.assertNull(req.getHeader("X-Forwarded-Proto")); + }); + } + + @Test + public void testRfc7239ForwardedHeaderChainPreservation(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setUseRfc7239(true) + .setForwardFor(true) + .setForwardProto(true) + ); + + runForwardedHeadersTest(ctx, options, null, req -> { + req.putHeader("Forwarded", "for=192.0.2.60;proto=https"); + }, req -> { + String forwarded = req.getHeader("Forwarded"); + ctx.assertNotNull(forwarded); + // Should contain both the original and the appended entry + List parts = List.of(forwarded.split(",\\s*")); + ctx.assertEquals(2, parts.size()); + ctx.assertTrue(parts.get(0).contains("for=192.0.2.60")); + ctx.assertTrue(parts.get(1).contains("for=")); + ctx.assertTrue(parts.get(1).contains("proto=http")); + }); + } + + @Test + public void testRfc7239ForOnlyEnabled(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setUseRfc7239(true) + .setForwardFor(true) + .setForwardProto(false) + .setForwardHost(false) + ); + + runForwardedHeadersTest(ctx, options, null, null, req -> { + String forwarded = req.getHeader("Forwarded"); + ctx.assertNotNull(forwarded); + ctx.assertTrue(forwarded.contains("for=")); + ctx.assertFalse(forwarded.contains("proto=")); + ctx.assertFalse(forwarded.contains("host=")); + }); + } + + @Test + public void testForwardedHeadersWithHttps(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(true) + .setForwardProto(true) + ); + + // Load test certificates from classpath (see src/test/resources/SSL_TEST_CERTIFICATES.txt) + PemKeyCertOptions pemOptions = new PemKeyCertOptions() + .setCertPath("server.cert.pem") + .setKeyPath("server.key.pem"); + + HttpServerOptions httpsServerOptions = new HttpServerOptions() + .setPort(8443) + .setHost("localhost") + .setSsl(true) + .setKeyCertOptions(pemOptions); + + runForwardedHeadersTest(ctx, options, httpsServerOptions, null, req -> { + String xForwardedProto = req.getHeader("X-Forwarded-Proto"); + ctx.assertNotNull(xForwardedProto); + ctx.assertEquals("https", xForwardedProto); + }); + } + + @Test + public void testEnabledFalseDoesNotAddHeaders(TestContext ctx) { + ProxyOptions options = new ProxyOptions() + .setForwardedHeadersOptions(new ForwardedHeadersOptions() + .setEnabled(false) // Explicitly disabled + .setForwardFor(true) + .setForwardProto(true) + ); + + runForwardedHeadersTest(ctx, options, null, null, req -> { + ctx.assertNull(req.getHeader("X-Forwarded-For")); + ctx.assertNull(req.getHeader("X-Forwarded-Proto")); + }); + } + + private void runForwardedHeadersTest(TestContext ctx, ProxyOptions options, HttpServerOptions serverOpts, Consumer requestCustomizer, Handler backendHandler) { + Async latch = ctx.async(); + + SocketAddress backend = startHttpBackend(ctx, 8081, req -> { + backendHandler.handle(req); + req.response().end("OK"); + }); + + if (options != null) { + proxyOptions = options; + } + if (serverOpts != null) { + serverOptions = serverOpts; + } + startProxy(backend); + + // Determine client settings based on server options + boolean isHttps = serverOpts != null && serverOpts.isSsl(); + int port = isHttps ? 8443 : 8080; + HttpClient client = isHttps + ? vertx.createHttpClient(new HttpClientOptions().setSsl(true).setTrustAll(true).setVerifyHost(false)) + : vertx.createHttpClient(); + + client + .request(HttpMethod.GET, port, "localhost", "/") + .compose(req -> { + if (requestCustomizer != null) { + requestCustomizer.accept(req); + } + return req.send().compose(HttpClientResponse::body); + }) + .onComplete(ctx.asyncAssertSuccess(body -> { + ctx.assertEquals("OK", body.toString()); + latch.complete(); + })); + + latch.await(); + } +} diff --git a/src/test/resources/SSL_TEST_CERTIFICATES.txt b/src/test/resources/SSL_TEST_CERTIFICATES.txt new file mode 100644 index 0000000..ca817bc --- /dev/null +++ b/src/test/resources/SSL_TEST_CERTIFICATES.txt @@ -0,0 +1,49 @@ +SSL Test Certificates for HTTPS Testing +========================================= + +This directory contains self-signed SSL certificates used for testing HTTPS functionality +in the vertx-http-proxy tests. + +Files: +------ +- server.cert.pem: Self-signed X.509 certificate for localhost +- server.key.pem: RSA private key for the certificate + +⚠️ WARNING: These certificates are for TESTING ONLY. Never use them in production! + +How to Regenerate: +------------------ + +If you need to regenerate these test certificates (e.g., if they expire), use the +following OpenSSL command: + + openssl req -x509 \ + -newkey rsa:2048 \ + -keyout server.key.pem \ + -out server.cert.pem \ + -days 3650 \ + -nodes \ + -subj "/CN=localhost" + +This will create: +- A 2048-bit RSA private key (server.key.pem) +- A self-signed certificate valid for 3650 days / 10 years (server.cert.pem) +- Certificate issued for CN=localhost +- No passphrase protection (-nodes flag) + +Certificate Details: +-------------------- +- Common Name (CN): localhost +- Key Size: 2048-bit RSA +- Validity: 10 years (3650 days) from generation date +- No passphrase protection + +Usage in Tests: +--------------- +These certificates are loaded in ForwardedHeadersTest.java using PemKeyCertOptions: + + PemKeyCertOptions pemOptions = new PemKeyCertOptions() + .setCertPath("server.cert.pem") + .setKeyPath("server.key.pem"); + +The files are loaded from the classpath (src/test/resources). diff --git a/src/test/resources/server.cert.pem b/src/test/resources/server.cert.pem new file mode 100644 index 0000000..abc2374 --- /dev/null +++ b/src/test/resources/server.cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUNuAiBe69Mh37+BiC8ZRR3PqKb8QwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMyNDE2MzMxOVoXDTM2MDMy +MTE2MzMxOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8Jy1xq6OUoo+AQ239nMZL1/+ZQ65ym8qi9Whfez8QY7m +hsOoMTmPAzlLFpt1dQ0mhfd+b/zIjqLnX1OIJ/xd+PQrkKtDJSztuZyodDeieJ0x +lB3gKqe+/ok9N5uNa14uYMe+uc7WX4F0iBTVk+Xszr8GmIIqBYi7pwEu1AeWEs1H +T6QCgCeJsvMIgqwGB2g3pXc1KI5L5HCnmvu5SKjdexw8MCVKIfEy5PYqS/9Zc1jW +7UVf6BOuobKyzRro9uDHqpSNOjtPW+5sjz/cA6fnB8HDj3ROWBUmE65ET2AC9FBD +uf9+Pokql4lCjDQgXKvdBYxqSilIHD+Lpt4ExNNnMQIDAQABo1MwUTAdBgNVHQ4E +FgQUEN6fbQMwP0b6WRaqtZ25Gj3FRy0wHwYDVR0jBBgwFoAUEN6fbQMwP0b6WRaq +tZ25Gj3FRy0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAKYoU +Gr+QSKjO7oDpqupLFhiTV7YYiuuzr7VT4FNffzle5F9Vd0krPs6rDdpFaDMwdzPc +WoD75K0FfypVQZKU7f3JxZo/3RNtP+VcoDVGlm2OPzyFw8dHKo7MBpd5Hn4eEoQt +MWJbFxMb89JAdbGCg9+UzFy93v9A2odS9ghlRKRBtazM5CodaofD2HAWRxLgJSnR +KpTMKxoBhHni9KssMzDGwtYqnfAkXWiLkYr9BpmguFeV7C+Gudz6rSqZISlxgF2V +KJBGNzVWKbu9SbEF30w+5WAhy8HbR/jrzdxMkj5H1v9awgAykv6LiQtzVKR4wNDm +ig8F7QUmhjIKF7V3BA== +-----END CERTIFICATE----- diff --git a/src/test/resources/server.key.pem b/src/test/resources/server.key.pem new file mode 100644 index 0000000..25210fa --- /dev/null +++ b/src/test/resources/server.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDwnLXGro5Sij4B +Dbf2cxkvX/5lDrnKbyqL1aF97PxBjuaGw6gxOY8DOUsWm3V1DSaF935v/MiOoudf +U4gn/F349CuQq0MlLO25nKh0N6J4nTGUHeAqp77+iT03m41rXi5gx765ztZfgXSI +FNWT5ezOvwaYgioFiLunAS7UB5YSzUdPpAKAJ4my8wiCrAYHaDeldzUojkvkcKea ++7lIqN17HDwwJUoh8TLk9ipL/1lzWNbtRV/oE66hsrLNGuj24MeqlI06O09b7myP +P9wDp+cHwcOPdE5YFSYTrkRPYAL0UEO5/34+iSqXiUKMNCBcq90FjGpKKUgcP4um +3gTE02cxAgMBAAECggEABJFxGQ3Q2WvS/Ami47yxLndT2N7T864zIFcEuyOP8X2x +Tj8yNwcI+k5w6MO4UenXQKVfguhJdX3Nxz9KfznZHeQi3JsHyhGljMfW4ef5f3E6 +kv2TNzQamfIIlcDwCSkPV9qK+jOdWdBXIktE98seeRli2mvkONks+OF6nl9rf71Z +DwJPXMc65zZdxT2SH81E2I7YJetf8/NwpkR1TT8IZEIERbkf6o1QtpM7D4j3CM/K +XYmwmTu+wMmIuviOTjQ22HRENmdrUgKiFE44EHafFBCpGfsy/QzSboye2VfnUx7P +6sr6KEv9ra1Iotp6kL0Tw0UWYD46vgS2w3jgCD82vQKBgQD/Ndd0g6yT5XP2gF7C +SX8R+muZWYMzHaxvCt8qkF/rXSd4YpqvOnVIPFpCjF4sFng7PyBdYHROanVerazD +2RWaYT6IaBZMumJhBs0q5t8lhsTqEPopZiQO3kvf+b8ZB6AxHSPpMq4pMmBFpyVX +WTy5t7VnL8jDerRc9l4nmTYGXQKBgQDxW04MCgphKaqxLu4VUh8gC7T/l8uzou/r +4TR2F1KJOrpgyh3JugSUoLMGF8li5x+iCtJtEh9AAoJng2piem1Lfx2cKet1Pw42 +cZmn+Hux+6qRdCDA89Uc5J47z1O5ajdlC0MvjpOq/6HMq6F/xj8ls5zBgTqCa3D+ +m//JMBcu5QKBgQCTjItuf5pNP4GV6I3ggiGHQWLoHApwiR1ibcSslR//sbHaNhf8 +ay3Xf50nkIP5tVv51PtfQR69H2uQO5UA/gcxZDuFHbiWz8OFGSuRPD2TMqGyOfKH +8Ne0CO2RJpFHZVNGUfSrJHPwuYFTg9lg8OTc8stcRxIpgk242W+CMWA8uQKBgQC2 +b6/FBenmm9o8aB7q2tJQJMlB0fnV5Tm5cNd41BQ9SrkK5HwzuXow5m+sEVrWeaG9 +mLSKYYJhngP4PquxmJz4zjMRkdY7H9icaq4c+4R0eqjpnYAMmcLjPiQlTYgZxSHu +LN8yGXGhde1Vif5fWPjuhJ/e3NHfN1uH+V8VMlazyQKBgQDdaTqWunEIjJAW/7FN +tfLR2aFIdjd+rEpFNNLab8wQzH5sntzCrQH+KHG7Wlaut8Xla/769QREnqVCnltb +MSRk2zym1w1vCHeDc+oyYbGuQXvSAi8AbYBcRYFcI34UIyh9ciGL1ZDSze0pl9fP +Vm4di2lr4bAPxEGScMKiyVPjOg== +-----END PRIVATE KEY-----