From f79309555ca528264528c294ea3145b25a28c7e3 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Wed, 18 Mar 2026 17:25:53 +0100 Subject: [PATCH 1/7] Refactor code for consistency and readability: - Removed unused constants and imports (`TRACK_NODE_STATUS`) from several classes. - Replaced explicit type declarations with `var` in tests and implementations for modernized code. - Improved test case clarity (`ProxySSLTest` and `HttpClientTest`) by refining method logic and assertions. - Minor code cleanup and enhanced null safety in `HttpClient` and connection handling. --- .../core/exchange/AbstractExchange.java | 7 +-- .../membrane/core/exchange/Exchange.java | 1 - .../balancer/ExchangeNodeStatusTracker.java | 3 - .../core/transport/http/Connection.java | 20 +++++-- .../core/transport/http/HttpClient.java | 58 ++++++++++++------- .../membrane/core/proxies/ProxySSLTest.java | 50 +++++++++------- .../core/proxies/ServiceProxyTest.java | 8 +-- .../core/transport/http/HttpClientTest.java | 15 ++--- 8 files changed, 95 insertions(+), 67 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/exchange/AbstractExchange.java b/core/src/main/java/com/predic8/membrane/core/exchange/AbstractExchange.java index 1f835a0aa9..655ca15a21 100644 --- a/core/src/main/java/com/predic8/membrane/core/exchange/AbstractExchange.java +++ b/core/src/main/java/com/predic8/membrane/core/exchange/AbstractExchange.java @@ -18,20 +18,17 @@ import com.predic8.membrane.core.http.*; import com.predic8.membrane.core.interceptor.*; import com.predic8.membrane.core.interceptor.Interceptor.*; -import com.predic8.membrane.core.interceptor.balancer.ExchangeNodeStatusTracker; +import com.predic8.membrane.core.interceptor.balancer.*; import com.predic8.membrane.core.model.*; -import com.predic8.membrane.core.proxies.Proxy; import com.predic8.membrane.core.proxies.*; +import com.predic8.membrane.core.proxies.Proxy; import org.slf4j.*; -import java.io.*; import java.net.*; import java.text.*; import java.util.*; -import static com.predic8.membrane.core.exchange.Exchange.TRACK_NODE_STATUS; import static com.predic8.membrane.core.exchange.ExchangeState.*; -import static java.lang.Boolean.TRUE; public abstract class AbstractExchange { private static final Logger log = LoggerFactory.getLogger(AbstractExchange.class.getName()); diff --git a/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java b/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java index 9381b85d12..314b20188c 100644 --- a/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java +++ b/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java @@ -37,7 +37,6 @@ public class Exchange extends AbstractExchange { * compatibility (i.e. for Java's internal HTTP client). */ public static final String ALLOW_H2 = "membrane.use.h2"; - public static final String TRACK_NODE_STATUS = "membrane.track.node.status"; public static final String SSL_CONTEXT = "membrane.ssl.context"; public static final String OAUTH2 = "membrane.oauth2"; public static final String SNI_SERVER_NAME = "membrane.sni.server.name"; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/ExchangeNodeStatusTracker.java b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/ExchangeNodeStatusTracker.java index 99ba5388b1..1523aaa71a 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/ExchangeNodeStatusTracker.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/balancer/ExchangeNodeStatusTracker.java @@ -14,9 +14,6 @@ package com.predic8.membrane.core.interceptor.balancer; -import static com.predic8.membrane.core.exchange.Exchange.TRACK_NODE_STATUS; -import static java.lang.Boolean.TRUE; - /** * Used by the {@link LoadBalancingInterceptor} to track the status of nodes during load balancing. * diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java b/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java index e51eeebf54..fb785a4020 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java @@ -77,6 +77,8 @@ public class Connection implements Closeable, MessageObserver, NonRelevantBodyOb private Exchange exchange; private boolean keepAttachedToExchange; + private boolean tunneled; + public static Connection open(String host, int port, String localHost, SSLProvider sslProvider, int connectTimeout) throws IOException { return open(host, port, localHost, sslProvider, null, connectTimeout); } @@ -120,18 +122,18 @@ public static Connection open(String host, int port, String localHost, SSLProvid log.debug("Opened connection on localPort: {}", con.socket.getLocalPort()); - setupStreams(con); + con.setupStreams(); return con; } - private static void setupStreams(Connection con) throws IOException { + private void setupStreams() throws IOException { if (ByteStreamLogging.isLoggingEnabled()) { String connectionName = chooseNewConnectionName(); - con.out = new BufferedOutputStream(wrapConnectionOutputStream(con.socket.getOutputStream(), connectionName + " out"), BUFFER_SIZE); - con.in = new BufferedInputStream(wrapConnectionInputStream(con.socket.getInputStream(), connectionName + " in"), BUFFER_SIZE); + out = new BufferedOutputStream(wrapConnectionOutputStream(socket.getOutputStream(), connectionName + " out"), BUFFER_SIZE); + in = new BufferedInputStream(wrapConnectionInputStream(socket.getInputStream(), connectionName + " in"), BUFFER_SIZE); } else { - con.out = new BufferedOutputStream(con.socket.getOutputStream(), BUFFER_SIZE); - con.in = new BufferedInputStream(con.socket.getInputStream(), BUFFER_SIZE); + out = new BufferedOutputStream(socket.getOutputStream(), BUFFER_SIZE); + in = new BufferedInputStream(socket.getInputStream(), BUFFER_SIZE); } } @@ -412,6 +414,8 @@ private void doTunnelHandshake(ProxyConfiguration proxy, Socket tunnel, String h replyStr = new String(reply, 0, replyLen); } + tunneled = true; + /* Look for '200 OK' response. Probably, some proxies may return HTTP/1.1 back */ if (!replyStr.startsWith("HTTP/1.0 200") && !replyStr.startsWith("HTTP/1.1 200")) { throw new IOException("Unable to tunnel through " @@ -451,4 +455,8 @@ private void doTunnelHandshake(ProxyConfiguration proxy, Socket tunnel, String h public SSLProvider getSslProvider() { return sslProvider; } + + public boolean getTunneled() { + return tunneled; + } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java index 2f06172ff2..1bf677ba20 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java @@ -21,10 +21,10 @@ import com.predic8.membrane.core.transport.http.client.protocol.*; import com.predic8.membrane.core.transport.http.streampump.*; import com.predic8.membrane.core.util.*; +import org.jetbrains.annotations.*; import org.slf4j.*; -import javax.annotation.*; -import java.io.*; +import javax.annotation.Nullable; import java.net.*; import static com.predic8.membrane.core.exchange.Exchange.*; @@ -74,9 +74,20 @@ public void call(Exchange exc) throws Exception { } private boolean dispatchCall(Exchange exc, String target, int attempt) throws Exception { - HostColonPort hcp = initializeRequest(exc, target); - - OutgoingConnectionType outConType = connectionFactory.getConnection(exc, hcp, attempt); + setAuthorizationHeader(exc); + HostColonPort hcp; + OutgoingConnectionType outConType; + try { + hcp = getHostColonPort(exc, target); + outConType = connectionFactory.getConnection(exc, hcp, attempt); + setRequestURI(exc.getRequest(), target, outConType.con()); + } catch (MalformedURLException e) { + // @TODO + throw new MalformedURLException(""" + The exchange's destination URI %s does not start with 'http'. Specify a 'target' within + the API configuration or make sure the exchanges destinations list contains a valid URI. + """.formatted(target)); + } if (configuration.getProxy() != null && outConType.sslProvider() == null) { // if we use a proxy for a plain HTTP (=non-HTTPS) request, attach the proxy credentials. @@ -93,36 +104,30 @@ private boolean dispatchCall(Exchange exc, String target, int attempt) throws Ex if (upgradedProtocol == null) return false; - log.debug("Upgrading to {}",upgradedProtocol); + log.debug("Upgrading to {}", upgradedProtocol); StreamPump.setupConnectionForwarding(exc, outConType.con(), upgradedProtocol, streamPumpStats); outConType.con().setExchange(exc); return true; } - HostColonPort initializeRequest(Exchange exc, String dest) throws IOException { - setRequestURI(exc.getRequest(), dest); + private void setAuthorizationHeader(Exchange exc) { if (configuration.getAuthentication() != null) exc.getRequest().getHeader().setAuthorization(configuration.getAuthentication().getUsername(), configuration.getAuthentication().getPassword()); + } - HostColonPort target = getTargetHostAndPort(exc.getRequest().isCONNECTRequest(), dest); + protected @NotNull HostColonPort getHostColonPort(Exchange exc, String dest) throws MalformedURLException { + var target = getTargetHostAndPort(exc.getRequest().isCONNECTRequest(), dest); if (configuration.isAdjustHostHeader() && (exc.getProxy() == null || exc.getProxy().isTargetAdjustHostHeader())) { - exc.getRequest().getHeader().setHost(target.toString()); + exc.getRequest().getHeader().setHost(target.toString()); // TODO } return target; } - public void setStreamPumpStats(StreamPump.StreamPumpStats streamPumpStats) { this.streamPumpStats = streamPumpStats; } - private static boolean trackNodeStatus(Exchange exc) { - if (exc.getProperty(TRACK_NODE_STATUS) instanceof Boolean status) - return status; - return false; - } - @Override public void close() { connectionFactory.getConnectionManager().shutdownWhenDone(); @@ -134,20 +139,33 @@ public ConnectionFactory getConnectionFactory() { return connectionFactory; } - void setRequestURI(Request req, String dest) throws MalformedURLException { + void setRequestURI(Request req, String dest, @NotNull Connection con) throws MalformedURLException { + // Only on none TLS connections + if (req.isCONNECTRequest()) { + req.setUri(getHostColonPort(dest)); + return; + } + // Use complete URL with protocol and host. The proxy needs to know where to forward - if (configuration.getProxy() != null || req.isCONNECTRequest()) { + if (configuration.getProxy() != null && !con.getTunneled()) { req.setUri(dest); return; } if (!dest.startsWith("http")) { throw new MalformedURLException(""" - The exchange's destination URI %s does not start with 'http'. Specify a within the API configuration or make sure the exchanges destinations list contains a valid URI. + The exchange's destination URI %s does not start with 'http'. Specify a 'target' within + the API configuration or make sure the exchanges destinations list contains a valid URI. """.formatted(dest)); } req.setUri(getPathAndQueryString(dest)); } + private static @NotNull String getHostColonPort(String dest) throws MalformedURLException { + return dest.startsWith("http") + ? HostColonPort.parse(dest).toString() + : dest; + } + /** * @param connect If true, do not use TLS even when the URL starts with https * @param dest URL diff --git a/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java b/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java index 00fe66ba52..f276b39bf7 100644 --- a/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java +++ b/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java @@ -31,23 +31,28 @@ import java.util.concurrent.atomic.*; import static com.predic8.membrane.core.exchange.Exchange.*; +import static com.predic8.membrane.core.http.Request.get; import static org.junit.jupiter.api.Assertions.*; public class ProxySSLTest { public static Collection data() { + // @formatter:off return Arrays.asList(new Object[][] { - { false, false, 3032, 3033 }, { false, true, 3034, 3035 }, { true, false, 3036, 3037 }, { true, true, 3038, 3039 } + // backend TLS, proxy TLS, backend port, proxy port + { false, false, 3032, 3033 }, + { false, true, 3034, 3035 }, + { true, false, 3036, 3037 }, + { true, true, 3038, 3039 } }); + // @formatter:on } @ParameterizedTest @MethodSource("data") void test(boolean backendUsesSSL, boolean proxyUsesSSL, int backendPort, int proxyPort) throws Exception { - DefaultRouter backend = createBackend(backendUsesSSL, backendPort); - - AtomicInteger proxyCounter = new AtomicInteger(); - - DefaultRouter proxy = createProxy(proxyUsesSSL, proxyPort, proxyCounter); + var backend = createBackend(backendUsesSSL, backendPort); + var proxyCounter = new AtomicInteger(); + var proxy = createProxy(proxyUsesSSL, proxyPort, proxyCounter); testClient(backendUsesSSL, backendPort, createAndConfigureClient(proxyUsesSSL, proxyPort), proxyCounter); @@ -56,9 +61,9 @@ void test(boolean backendUsesSSL, boolean proxyUsesSSL, int backendPort, int pro } private static @NotNull DefaultRouter createProxy(boolean proxyUsesSSL, int proxyPort, AtomicInteger proxyCounter) throws IOException { - DefaultRouter proxy = new DefaultRouter(); + var proxy = new DefaultRouter(); proxy.getConfiguration().setHotDeploy(false); - ProxyRule rule = new ProxyRule(new ProxyRuleKey(proxyPort)); + var rule = new ProxyRule(new ProxyRuleKey(proxyPort)); rule.getFlow().add(new AbstractInterceptor() { @Override public Outcome handleRequest(Exchange exc) { @@ -76,9 +81,9 @@ public Outcome handleRequest(Exchange exc) { private static void testClient(boolean backendUsesSSL, int backendPort, HttpClient hc, AtomicInteger proxyCounter) throws Exception { // Step 4: Test client - Exchange exc = new Request.Builder().get("http" + (backendUsesSSL ? "s" : "") + "://localhost:" + backendPort + "/foo").buildExchange(); + var exc = get("http%s://localhost:%d/foo".formatted(backendUsesSSL ? "s" : "", backendPort)).buildExchange(); if (backendUsesSSL) { - SSLParser ssl = new SSLParser(); + var ssl = new SSLParser(); ssl.setTrustStore(new TrustStore()); ssl.getTrustStore().setLocation("classpath:/ssl-rsa-pub.keystore"); ssl.getTrustStore().setPassword("secret"); @@ -93,27 +98,30 @@ private static void testClient(boolean backendUsesSSL, int backendPort, HttpClie private static @NotNull HttpClient createAndConfigureClient(boolean proxyUsesSSL, int proxyPort) { // Step 3: configure the client to access the backend through the proxy - HttpClientConfiguration httpClientConfiguration = new HttpClientConfiguration(); - ProxyConfiguration proxyConfiguration = new ProxyConfiguration(); - proxyConfiguration.setHost("localhost"); - proxyConfiguration.setPort(proxyPort); + var hcc = new HttpClientConfiguration(); + var pc = new ProxyConfiguration(); + pc.setHost("localhost"); + pc.setPort(proxyPort); if (proxyUsesSSL) { - SSLParser ssl = new SSLParser(); + var ssl = new SSLParser(); ssl.setTrustStore(new TrustStore()); ssl.getTrustStore().setLocation("classpath:/ssl-rsa-pub2.keystore"); ssl.getTrustStore().setPassword("secret"); ssl.setEndpointIdentificationAlgorithm(""); // workarond the fact that the certificate was not issued for 'localhost' - proxyConfiguration.setSslParser(ssl); + pc.setSslParser(ssl); } - httpClientConfiguration.setProxy(proxyConfiguration); - return new HttpClient(httpClientConfiguration); + hcc.setProxy(pc); + return new HttpClient(hcc); } private static @NotNull DefaultRouter createBackend(boolean backendUsesSSL, int backendPort) throws IOException { // Step 1: create the backend - DefaultRouter backend = new DefaultRouter(); + var backend = new DefaultRouter(); backend.getConfiguration().setHotDeploy(false); - ServiceProxy sp = new ServiceProxy(new ServiceProxyKey(backendPort), null, 0); + var ruleKey = new ServiceProxyKey(backendPort); + ruleKey.setPath("/foo"); + ruleKey.setUsePathPattern(true); + var sp = new ServiceProxy(ruleKey, null, 0); if (backendUsesSSL) { sp.setSslInboundParser(getSslParser("classpath:/ssl-rsa.keystore")); } @@ -124,7 +132,7 @@ private static void testClient(boolean backendUsesSSL, int backendPort, HttpClie } private static @NotNull SSLParser getSslParser(String location) { - SSLParser ssl = new SSLParser(); + var ssl = new SSLParser(); ssl.setKeyStore(new KeyStore()); ssl.getKeyStore().setLocation(location); ssl.getKeyStore().setKeyPassword("secret"); diff --git a/core/src/test/java/com/predic8/membrane/core/proxies/ServiceProxyTest.java b/core/src/test/java/com/predic8/membrane/core/proxies/ServiceProxyTest.java index 74db74f0be..f16e1df8f5 100644 --- a/core/src/test/java/com/predic8/membrane/core/proxies/ServiceProxyTest.java +++ b/core/src/test/java/com/predic8/membrane/core/proxies/ServiceProxyTest.java @@ -29,9 +29,9 @@ class ServiceProxyTest { private static Router router; @BeforeAll - public static void setup() throws Exception { + static void setup() throws Exception { router = new TestRouter(); - APIProxy proxyWithOutTarget = new APIProxy() {{ + var proxyWithOutTarget = new APIProxy() {{ key = new APIProxyKey(2000); }}; router.add(proxyWithOutTarget); @@ -39,7 +39,7 @@ public static void setup() throws Exception { } @AfterAll - public static void shutdown() { + static void shutdown() { router.stop(); } @@ -54,7 +54,7 @@ void foo() { .contentType(APPLICATION_PROBLEM_JSON) .body("type", equalTo("https://membrane-api.io/problems/user")) .body("detail", Matchers.containsString("/foo")) - .body("detail", Matchers.containsString("")); + .body("detail", Matchers.containsString("target")); // @formatter:on } diff --git a/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java b/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java index 981029bcb7..28a7d92d1d 100644 --- a/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java +++ b/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java @@ -15,9 +15,10 @@ package com.predic8.membrane.core.transport.http; import com.predic8.membrane.core.exchange.*; -import com.predic8.membrane.core.http.*; import org.junit.jupiter.api.*; +import org.mockito.*; +import static com.predic8.membrane.core.http.Request.get; import static org.junit.jupiter.api.Assertions.*; class HttpClientTest { @@ -31,22 +32,22 @@ void setUp() { @Test void init() throws Exception { - Exchange exc = Request.get("/foo").buildExchange(); - client.initializeRequest(exc,"https://example.com"); + Exchange exc = get("/foo").buildExchange(); + client.getHostColonPort(exc,"https://example.com"); assertEquals("example.com:443",exc.getRequest().getHeader().getHost()); } @Test void setRequestURISimple() throws Exception { - var request = Request.get("/bar").build(); - client.setRequestURI(request, "https://predic8.de/foo"); + var request = get("/bar").build(); + client.setRequestURI(request, "https://predic8.de/foo", Mockito.mock(Connection.class)); assertEquals("/foo", request.getUri()); } @Test void setRequestURIWithoutPath() throws Exception { - var request = Request.get("/bar").build(); - client.setRequestURI(request, "https://predic8.de"); + var request = get("/bar").build(); + client.setRequestURI(request, "https://predic8.de", Mockito.mock(Connection.class)); assertEquals("/", request.getUri()); } } \ No newline at end of file From 0027a93bed3d11bf3243360f07d62c886852eb27 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Wed, 18 Mar 2026 17:27:11 +0100 Subject: [PATCH 2/7] Remove unused logger from `Exchange` and cleanup unused imports in `HttpClient`. --- .../java/com/predic8/membrane/core/exchange/Exchange.java | 2 -- .../predic8/membrane/core/transport/http/HttpClient.java | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java b/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java index 314b20188c..fdb28183ed 100644 --- a/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java +++ b/core/src/main/java/com/predic8/membrane/core/exchange/Exchange.java @@ -43,8 +43,6 @@ public class Exchange extends AbstractExchange { public static final String WS_ORIGINAL_EXCHANGE = "membrane.ws.original.exchange"; public static final String SECURITY_SCHEMES = "membrane.security.schemes"; - private static final Logger log = LoggerFactory.getLogger(Exchange.class.getName()); - private AbstractHttpHandler handler; private String originalHostHeader = ""; diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java index 1bf677ba20..5d45a8ee8d 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java @@ -27,7 +27,6 @@ import javax.annotation.Nullable; import java.net.*; -import static com.predic8.membrane.core.exchange.Exchange.*; import static com.predic8.membrane.core.transport.http.client.protocol.Http2ProtocolHandler.*; import static com.predic8.membrane.core.util.HttpUtil.*; @@ -84,7 +83,7 @@ private boolean dispatchCall(Exchange exc, String target, int attempt) throws Ex } catch (MalformedURLException e) { // @TODO throw new MalformedURLException(""" - The exchange's destination URI %s does not start with 'http'. Specify a 'target' within + The exchange's destination URI %s does not start with 'http'. Specify a 'target' within the API configuration or make sure the exchanges destinations list contains a valid URI. """.formatted(target)); } @@ -153,7 +152,7 @@ void setRequestURI(Request req, String dest, @NotNull Connection con) throws Mal } if (!dest.startsWith("http")) { throw new MalformedURLException(""" - The exchange's destination URI %s does not start with 'http'. Specify a 'target' within + The exchange's destination URI %s does not start with 'http'. Specify a 'target' within the API configuration or make sure the exchanges destinations list contains a valid URI. """.formatted(dest)); } @@ -170,7 +169,6 @@ void setRequestURI(Request req, String dest, @NotNull Connection con) throws Mal * @param connect If true, do not use TLS even when the URL starts with https * @param dest URL * @return HostColonPort - * @throws MalformedURLException */ private HostColonPort getTargetHostAndPort(boolean connect, String dest) throws MalformedURLException { if (connect) From acd155f3ecf72d98bad96b9473005dd01cb9707b Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 19 Mar 2026 11:40:39 +0100 Subject: [PATCH 3/7] Refactor `HttpClient` and related classes for improved maintainability: - Extracted and encapsulated logic into `adjustHostHeader` method for cleaner host header adjustments. - Enhanced null safety and annotations using `@NotNull` for better code guarantees. - Replaced explicit type declarations with `var` for modernized and consistent code. - Simplified `tunneled` proxy handling in `Connection`. - Improved error message formatting and streamlined exception handling in `getHostColonPort`. - Minor comments and formatting adjustments for readability across test and core implementations. --- .../core/transport/http/Connection.java | 27 +++++---- .../core/transport/http/HostColonPort.java | 4 +- .../core/transport/http/HttpClient.java | 55 ++++++++----------- .../membrane/core/proxies/ProxySSLTest.java | 2 +- .../core/transport/http/HttpClientTest.java | 4 +- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java b/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java index fb785a4020..ae7df475a9 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java @@ -77,6 +77,9 @@ public class Connection implements Closeable, MessageObserver, NonRelevantBodyOb private Exchange exchange; private boolean keepAttachedToExchange; + /** + * If the connection is tunneled through a proxy + */ private boolean tunneled; public static Connection open(String host, int port, String localHost, SSLProvider sslProvider, int connectTimeout) throws IOException { @@ -414,16 +417,13 @@ private void doTunnelHandshake(ProxyConfiguration proxy, Socket tunnel, String h replyStr = new String(reply, 0, replyLen); } - tunneled = true; - /* Look for '200 OK' response. Probably, some proxies may return HTTP/1.1 back */ if (!replyStr.startsWith("HTTP/1.0 200") && !replyStr.startsWith("HTTP/1.1 200")) { - throw new IOException("Unable to tunnel through " - + proxy.getHost() + ":" + proxy.getPort() - + ". Proxy returns \"" + replyStr + "\""); + throw new IOException("Unable to tunnel through %s:%d. Proxy returns \"%s\"".formatted(proxy.getHost(), proxy.getPort(), replyStr)); } - /* tunneling Handshake was successful! */ + /* tunneling Handshake was successful! */ + tunneled = true; } private static @NotNull String getHostString(ProxyConfiguration proxy) { @@ -431,10 +431,8 @@ private void doTunnelHandshake(ProxyConfiguration proxy, Socket tunnel, String h } private static byte @NotNull [] createConnectMessage(ProxyConfiguration proxy, String host, int port) { - String msg = "CONNECT " + host + ":" + port + " HTTP/1.0\r\n" - + "User-Agent: " + USERAGENT + "\r\n" - + (proxy.isAuthentication() ? ("Proxy-Authorization: " + proxy.getCredentials() + "\r\n") : "") - + "\r\n"; + var msg = "CONNECT %s:%d HTTP/1.0\r\nUser-Agent: %s\r\n%s\r\n" + .formatted(host, port, USERAGENT, getProxyAuthenticationHeader(proxy)); byte[] b; try { /* @@ -452,11 +450,18 @@ private void doTunnelHandshake(ProxyConfiguration proxy, Socket tunnel, String h return b; } + private static @NotNull String getProxyAuthenticationHeader(ProxyConfiguration proxy) { + return proxy.isAuthentication() ? ("Proxy-Authorization: " + proxy.getCredentials() + "\r\n") : ""; + } + public SSLProvider getSslProvider() { return sslProvider; } - public boolean getTunneled() { + /** + * @return true If the connection is tunneled through a proxy + */ + public boolean isTunneled() { return tunneled; } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java index 967fed139a..99397af358 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java @@ -13,6 +13,8 @@ limitations under the License. */ package com.predic8.membrane.core.transport.http; +import org.jetbrains.annotations.*; + import java.net.*; import java.util.regex.*; @@ -52,7 +54,7 @@ public URI toURI() throws URISyntaxException { } @Override - public String toString() { + public @NotNull String toString() { return host + ":" + port; } diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java index 5d45a8ee8d..6e56052db6 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java @@ -25,6 +25,7 @@ import org.slf4j.*; import javax.annotation.Nullable; +import java.io.*; import java.net.*; import static com.predic8.membrane.core.transport.http.client.protocol.Http2ProtocolHandler.*; @@ -74,19 +75,10 @@ public void call(Exchange exc) throws Exception { private boolean dispatchCall(Exchange exc, String target, int attempt) throws Exception { setAuthorizationHeader(exc); - HostColonPort hcp; - OutgoingConnectionType outConType; - try { - hcp = getHostColonPort(exc, target); - outConType = connectionFactory.getConnection(exc, hcp, attempt); - setRequestURI(exc.getRequest(), target, outConType.con()); - } catch (MalformedURLException e) { - // @TODO - throw new MalformedURLException(""" - The exchange's destination URI %s does not start with 'http'. Specify a 'target' within - the API configuration or make sure the exchanges destinations list contains a valid URI. - """.formatted(target)); - } + var hcp = getHostColonPort(exc, target); + adjustHostHeader(exc, hcp); + var outConType = connectionFactory.getConnection(exc, hcp, attempt); + setRequestURI(exc.getRequest(), target, outConType.con()); if (configuration.getProxy() != null && outConType.sslProvider() == null) { // if we use a proxy for a plain HTTP (=non-HTTPS) request, attach the proxy credentials. @@ -99,7 +91,7 @@ private boolean dispatchCall(Exchange exc, String target, int attempt) throws Ex exc.getNodeStatusTracker().setNodeStatusCode(attempt, exc.getResponse().getStatusCode()); // Check for protocol upgrades - String upgradedProtocol = exc.getProperty(UPGRADED_PROTOCOL, String.class); + var upgradedProtocol = exc.getProperty(UPGRADED_PROTOCOL, String.class); if (upgradedProtocol == null) return false; @@ -110,17 +102,29 @@ private boolean dispatchCall(Exchange exc, String target, int attempt) throws Ex return true; } + protected void adjustHostHeader(Exchange exc, HostColonPort target) { + if (configuration.isAdjustHostHeader() && (exc.getProxy() == null || exc.getProxy().isTargetAdjustHostHeader())) { + exc.getRequest().getHeader().setHost(target.toString()); + } + } + private void setAuthorizationHeader(Exchange exc) { if (configuration.getAuthentication() != null) exc.getRequest().getHeader().setAuthorization(configuration.getAuthentication().getUsername(), configuration.getAuthentication().getPassword()); } protected @NotNull HostColonPort getHostColonPort(Exchange exc, String dest) throws MalformedURLException { - var target = getTargetHostAndPort(exc.getRequest().isCONNECTRequest(), dest); - if (configuration.isAdjustHostHeader() && (exc.getProxy() == null || exc.getProxy().isTargetAdjustHostHeader())) { - exc.getRequest().getHeader().setHost(target.toString()); // TODO + if (exc.getRequest().isCONNECTRequest()) + return new HostColonPort(false, dest); + + try { + return HostColonPort.parse(dest); + } catch (MalformedURLException e) { + throw new MalformedURLException(""" + The exchange's destination URI %s does not start with 'http'. Specify a 'target' within + the API configuration or make sure the exchanges destinations list contains a valid URI. + """.formatted(dest)); } - return target; } public void setStreamPumpStats(StreamPump.StreamPumpStats streamPumpStats) { @@ -146,7 +150,7 @@ void setRequestURI(Request req, String dest, @NotNull Connection con) throws Mal } // Use complete URL with protocol and host. The proxy needs to know where to forward - if (configuration.getProxy() != null && !con.getTunneled()) { + if (configuration.getProxy() != null && !con.isTunneled()) { req.setUri(dest); return; } @@ -159,23 +163,12 @@ void setRequestURI(Request req, String dest, @NotNull Connection con) throws Mal req.setUri(getPathAndQueryString(dest)); } - private static @NotNull String getHostColonPort(String dest) throws MalformedURLException { + protected static @NotNull String getHostColonPort(String dest) throws MalformedURLException { return dest.startsWith("http") ? HostColonPort.parse(dest).toString() : dest; } - /** - * @param connect If true, do not use TLS even when the URL starts with https - * @param dest URL - * @return HostColonPort - */ - private HostColonPort getTargetHostAndPort(boolean connect, String dest) throws MalformedURLException { - if (connect) - return new HostColonPort(false, dest); - return HostColonPort.parse(dest); - } - // TODO Rewrite all clients to use try with resources and then remove it @Override protected void finalize() { diff --git a/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java b/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java index f276b39bf7..153b695ed3 100644 --- a/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java +++ b/core/src/test/java/com/predic8/membrane/core/proxies/ProxySSLTest.java @@ -87,7 +87,7 @@ private static void testClient(boolean backendUsesSSL, int backendPort, HttpClie ssl.setTrustStore(new TrustStore()); ssl.getTrustStore().setLocation("classpath:/ssl-rsa-pub.keystore"); ssl.getTrustStore().setPassword("secret"); - ssl.setEndpointIdentificationAlgorithm(""); // workarond the fact that the certificate was not issued for 'localhost' + ssl.setEndpointIdentificationAlgorithm(""); // Workaround the fact that the certificate was not issued for 'localhost' exc.setProperty(SSL_CONTEXT, new StaticSSLContext(ssl, new ResolverMap(), null)); } hc.call(exc); diff --git a/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java b/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java index 28a7d92d1d..9ab6c0aacb 100644 --- a/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java +++ b/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java @@ -32,8 +32,8 @@ void setUp() { @Test void init() throws Exception { - Exchange exc = get("/foo").buildExchange(); - client.getHostColonPort(exc,"https://example.com"); + var exc = get("/foo").buildExchange(); + client.adjustHostHeader(exc, new HostColonPort(false,client.getHostColonPort( "https://example.com"))); assertEquals("example.com:443",exc.getRequest().getHeader().getHost()); } From 581498aa44e740998a6c705af58a0463a067e0a1 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 19 Mar 2026 12:14:35 +0100 Subject: [PATCH 4/7] Update license headers and fix method reference in `HttpClientTest` - Added missing Apache 2.0 license headers to `AbstractFlowInterceptorTest`. - Replaced direct call to `client.getHostColonPort` with static `HttpClient.getHostColonPort` in `HttpClientTest`. --- .../flow/AbstractFlowInterceptorTest.java | 14 ++++++++++++++ .../core/transport/http/HttpClientTest.java | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/AbstractFlowInterceptorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/AbstractFlowInterceptorTest.java index d2e6035d3d..c41f5008e1 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/flow/AbstractFlowInterceptorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/flow/AbstractFlowInterceptorTest.java @@ -1,3 +1,17 @@ +/* Copyright 2026 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.core.interceptor.flow; import com.predic8.membrane.core.interceptor.log.*; diff --git a/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java b/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java index 9ab6c0aacb..e6fa562082 100644 --- a/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java +++ b/core/src/test/java/com/predic8/membrane/core/transport/http/HttpClientTest.java @@ -33,7 +33,7 @@ void setUp() { @Test void init() throws Exception { var exc = get("/foo").buildExchange(); - client.adjustHostHeader(exc, new HostColonPort(false,client.getHostColonPort( "https://example.com"))); + client.adjustHostHeader(exc, new HostColonPort(false,HttpClient.getHostColonPort( "https://example.com"))); assertEquals("example.com:443",exc.getRequest().getHeader().getHost()); } From 5e01946701fcac4fc74b7f3097899cee5d7ff744 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 19 Mar 2026 14:19:52 +0100 Subject: [PATCH 5/7] comments, test --- .../membrane/core/transport/http/Connection.java | 4 ++-- .../core/transport/http/HostColonPort.java | 15 +++++++++++++-- .../membrane/core/transport/http/HttpClient.java | 8 +++++++- .../core/transport/http/client/RetryHandler.java | 2 +- .../core/transport/http/HostColonPortTest.java | 13 +++++++++++-- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java b/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java index ae7df475a9..e06f183496 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/Connection.java @@ -78,7 +78,7 @@ public class Connection implements Closeable, MessageObserver, NonRelevantBodyOb private boolean keepAttachedToExchange; /** - * If the connection is tunneled through a proxy + * If the connection is tunneled through a proxy and the connection to the real backend uses TLS */ private boolean tunneled; @@ -459,7 +459,7 @@ public SSLProvider getSslProvider() { } /** - * @return true If the connection is tunneled through a proxy + * @return true If the connection is tunneled through a proxy and the connection to the real backend uses TLS */ public boolean isTunneled() { return tunneled; diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java index 99397af358..f6af65832c 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HostColonPort.java @@ -22,6 +22,12 @@ public record HostColonPort(boolean useSSL, String host, int port) { private static final Pattern pattern = Pattern.compile("^(https?)://([^:/#]+)(?::(\\d+))?.*"); + /** + * + * @param useSSL To compute default ports + * @param host Hostname, e.g. api.predic8.de, 127.0.0.1, [3:34:55] + * @param port port number + */ public HostColonPort(boolean useSSL, String host, int port) { this.useSSL = useSSL; this.host = host; @@ -32,6 +38,11 @@ public HostColonPort(boolean useSSL, String host, int port) { } } + /** + * + * @param useSSL To compute default ports + * @param hostAndPort e.g. api.predic8.de:443 + */ public HostColonPort(boolean useSSL, String hostAndPort) { this(useSSL, hostPart(hostAndPort), portPart(hostAndPort, useSSL ? 443 : 80)); } @@ -59,12 +70,12 @@ public URI toURI() throws URISyntaxException { } private static String hostPart(String addr) { - var colon = addr.indexOf(":"); + var colon = addr.lastIndexOf(":"); return (colon > -1 ? addr.substring(0, colon) : addr); } private static int portPart(String addr, int defaultValue) { - var colon = addr.indexOf(":"); + var colon = addr.lastIndexOf(":"); return (colon > -1 ? Integer.parseInt(addr.substring(colon + 1)) : defaultValue); } diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java index 6e56052db6..4ade34e2e6 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java @@ -67,7 +67,7 @@ public HttpClient(@Nullable HttpClientConfiguration clientConfiguration, @Nullab } public void call(Exchange exc) throws Exception { - ProtocolHandler ph = protocolHandlerFactory.getHandler(exc, exc.getRequest().getHeader().getUpgradeProtocol()); + var ph = protocolHandlerFactory.getHandler(exc, exc.getRequest().getHeader().getUpgradeProtocol()); ph.checkUpgradeRequest(exc); configuration.getRetryHandler().executeWithRetries(exc, this::dispatchCall); ph.cleanup(exc); @@ -113,6 +113,12 @@ private void setAuthorizationHeader(Exchange exc) { exc.getRequest().getHeader().setAuthorization(configuration.getAuthentication().getUsername(), configuration.getAuthentication().getPassword()); } + /** + * @param exc Exchange + * @param dest URL for normal requests and host:port for CONNECT requests + * @return HostColonPort + * @throws MalformedURLException + */ protected @NotNull HostColonPort getHostColonPort(Exchange exc, String dest) throws MalformedURLException { if (exc.getRequest().isCONNECTRequest()) return new HostColonPort(false, dest); diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java b/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java index 705933defe..39d71ae8df 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/client/RetryHandler.java @@ -85,7 +85,7 @@ public void executeWithRetries(Exchange exc, RetryableCall call) throws Exceptio Exception exceptionInLastCall = null; double currentDelay = delay; for (int attempt = 0; attempt <= retries; attempt++) { - String dest = getDestination(exc, attempt); + var dest = getDestination(exc, attempt); log.debug("Attempt #{} from #{} to {}", attempt, retries + 1, dest); try { if (call.execute(exc, dest, attempt)) { diff --git a/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java b/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java index a5374bbbe7..82f4272d60 100644 --- a/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java +++ b/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java @@ -17,7 +17,7 @@ import java.net.*; -import static com.predic8.membrane.core.transport.http.HostColonPort.parse; +import static com.predic8.membrane.core.transport.http.HostColonPort.*; import static org.junit.jupiter.api.Assertions.*; @SuppressWarnings("HttpUrlsUsage") @@ -28,7 +28,7 @@ public class HostColonPortTest { static final HostColonPort HCP_HTTPS_LOCALHOST = new HostColonPort(true, "localhost", 443); static final HostColonPort HCP_HTTPS_LOCALHOST_8443 = new HostColonPort(true, "localhost", 8443); - static URI uriLocalhost, uriLocalhost8080, uriHttpsLocalhost, uriHttpsLocalhost8443; + static URI uriLocalhost, uriLocalhost8080, uriHttpsLocalhost, uriHttpsLocalhost8443; @BeforeAll static void setUp() throws URISyntaxException { @@ -80,6 +80,15 @@ void toURITests() throws URISyntaxException { assertEquals(uriHttpsLocalhost8443, HCP_HTTPS_LOCALHOST_8443.toURI()); } + + @Test + void ipv6() throws URISyntaxException { + var hcp = new HostColonPort(false, "[2001:db8::1]:443"); + assertEquals("[2001:db8::1]", hcp.host()); + assertEquals(443, hcp.port()); + } + + @Nested class Parse { From 00d6e52a780cc95fe7cb8d5a7ab99ef0d029453a Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 19 Mar 2026 14:20:49 +0100 Subject: [PATCH 6/7] comments, test --- .../com/predic8/membrane/core/transport/http/HttpClient.java | 1 - .../predic8/membrane/core/transport/http/HostColonPortTest.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java index 4ade34e2e6..819af6b7fd 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java @@ -117,7 +117,6 @@ private void setAuthorizationHeader(Exchange exc) { * @param exc Exchange * @param dest URL for normal requests and host:port for CONNECT requests * @return HostColonPort - * @throws MalformedURLException */ protected @NotNull HostColonPort getHostColonPort(Exchange exc, String dest) throws MalformedURLException { if (exc.getRequest().isCONNECTRequest()) diff --git a/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java b/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java index 82f4272d60..d0e8711c2a 100644 --- a/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java +++ b/core/src/test/java/com/predic8/membrane/core/transport/http/HostColonPortTest.java @@ -82,7 +82,7 @@ void toURITests() throws URISyntaxException { @Test - void ipv6() throws URISyntaxException { + void ipv6() { var hcp = new HostColonPort(false, "[2001:db8::1]:443"); assertEquals("[2001:db8::1]", hcp.host()); assertEquals(443, hcp.port()); From 40cdb8470d8c015e919b55d425af0d2e0e36c992 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 19 Mar 2026 15:25:05 +0100 Subject: [PATCH 7/7] Exception text --- .../com/predic8/membrane/core/transport/http/HttpClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java index 819af6b7fd..c156f27b48 100644 --- a/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java +++ b/core/src/main/java/com/predic8/membrane/core/transport/http/HttpClient.java @@ -126,7 +126,7 @@ private void setAuthorizationHeader(Exchange exc) { return HostColonPort.parse(dest); } catch (MalformedURLException e) { throw new MalformedURLException(""" - The exchange's destination URI %s does not start with 'http'. Specify a 'target' within + The exchange's destination URI %s is not valid. Specify a 'target' within the API configuration or make sure the exchanges destinations list contains a valid URI. """.formatted(dest)); }