Skip to content

Commit 21bdf94

Browse files
committed
Release version 5.3.2
2 parents bc39167 + d935ada commit 21bdf94

File tree

3 files changed

+175
-3
lines changed

3 files changed

+175
-3
lines changed

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>com.atomgraph</groupId>
55
<artifactId>linkeddatahub</artifactId>
6-
<version>5.3.1</version>
6+
<version>5.3.2</version>
77
<packaging>${packaging.type}</packaging>
88

99
<name>AtomGraph LinkedDataHub</name>
@@ -46,7 +46,7 @@
4646
<url>https://github.com/AtomGraph/LinkedDataHub</url>
4747
<connection>scm:git:git://github.com/AtomGraph/LinkedDataHub.git</connection>
4848
<developerConnection>scm:git:git@github.com:AtomGraph/LinkedDataHub.git</developerConnection>
49-
<tag>linkeddatahub-5.3.1</tag>
49+
<tag>linkeddatahub-5.3.2</tag>
5050
</scm>
5151

5252
<repositories>

src/main/java/com/atomgraph/linkeddatahub/client/filter/ClientUriRewriteFilter.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,18 @@ public void filter(ClientRequestContext cr) throws IOException
7070
String newScheme = cr.getUri().getScheme();
7171
if (getProxyScheme() != null) newScheme = getProxyScheme();
7272

73+
// Preserve subdomain prefix only when proxyHost is the same domain as host, to prevent
74+
// the HTTP client reusing a connection with a different TLS SNI (which causes 421).
75+
// When proxyHost is a distinct internal hostname (e.g. "nginx"), no collision is possible.
76+
String newHost = getProxyHost();
77+
if (cr.getUri().getHost().endsWith("." + getHost()) && getProxyHost().equals(getHost()))
78+
{
79+
String subdomainPrefix = cr.getUri().getHost().substring(0, cr.getUri().getHost().length() - getHost().length()); // e.g. "admin."
80+
newHost = subdomainPrefix + getProxyHost();
81+
}
82+
7383
// cannot use the URI class because query string with special chars such as '+' gets decoded
74-
URI newUri = UriBuilder.fromUri(cr.getUri()).scheme(newScheme).host(getProxyHost()).port(getProxyPort()).build();
84+
URI newUri = UriBuilder.fromUri(cr.getUri()).scheme(newScheme).host(newHost).port(getProxyPort()).build();
7585

7686
if (log.isDebugEnabled()) log.debug("Rewriting client request URI from '{}' to '{}'", cr.getUri(), newUri);
7787
cr.setUri(newUri);
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Copyright 2021 Martynas Jusevičius <martynas@atomgraph.com>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.atomgraph.linkeddatahub.client.filter;
18+
19+
import jakarta.ws.rs.client.Client;
20+
import jakarta.ws.rs.client.ClientRequestContext;
21+
import jakarta.ws.rs.core.Configuration;
22+
import jakarta.ws.rs.core.Cookie;
23+
import jakarta.ws.rs.core.MediaType;
24+
import jakarta.ws.rs.core.MultivaluedHashMap;
25+
import jakarta.ws.rs.core.MultivaluedMap;
26+
import jakarta.ws.rs.core.Response;
27+
import java.io.IOException;
28+
import java.io.OutputStream;
29+
import java.lang.annotation.Annotation;
30+
import java.lang.reflect.Type;
31+
import java.net.URI;
32+
import java.util.Collection;
33+
import java.util.Date;
34+
import java.util.List;
35+
import java.util.Locale;
36+
import java.util.Map;
37+
import org.junit.Test;
38+
import static org.junit.Assert.*;
39+
40+
/**
41+
* Unit tests for {@link ClientUriRewriteFilter}.
42+
*
43+
* @author {@literal Martynas Jusevičius <martynas@atomgraph.com>}
44+
*/
45+
public class ClientUriRewriteFilterTest
46+
{
47+
48+
private static class StubRequestContext implements ClientRequestContext
49+
{
50+
private URI uri;
51+
private final MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
52+
53+
StubRequestContext(URI uri) { this.uri = uri; }
54+
55+
@Override public URI getUri() { return uri; }
56+
@Override public void setUri(URI uri) { this.uri = uri; }
57+
@Override public MultivaluedMap<String, Object> getHeaders() { return headers; }
58+
59+
@Override public Object getProperty(String name) { throw new UnsupportedOperationException(); }
60+
@Override public Collection<String> getPropertyNames() { throw new UnsupportedOperationException(); }
61+
@Override public void setProperty(String name, Object object) { throw new UnsupportedOperationException(); }
62+
@Override public void removeProperty(String name) { throw new UnsupportedOperationException(); }
63+
@Override public String getMethod() { throw new UnsupportedOperationException(); }
64+
@Override public void setMethod(String method) { throw new UnsupportedOperationException(); }
65+
@Override public MultivaluedMap<String, String> getStringHeaders() { throw new UnsupportedOperationException(); }
66+
@Override public String getHeaderString(String name) { throw new UnsupportedOperationException(); }
67+
@Override public Date getDate() { throw new UnsupportedOperationException(); }
68+
@Override public Locale getLanguage() { throw new UnsupportedOperationException(); }
69+
@Override public MediaType getMediaType() { throw new UnsupportedOperationException(); }
70+
@Override public List<MediaType> getAcceptableMediaTypes() { throw new UnsupportedOperationException(); }
71+
@Override public List<Locale> getAcceptableLanguages() { throw new UnsupportedOperationException(); }
72+
@Override public Map<String, Cookie> getCookies() { throw new UnsupportedOperationException(); }
73+
@Override public boolean hasEntity() { throw new UnsupportedOperationException(); }
74+
@Override public Object getEntity() { throw new UnsupportedOperationException(); }
75+
@Override public Class<?> getEntityClass() { throw new UnsupportedOperationException(); }
76+
@Override public Type getEntityType() { throw new UnsupportedOperationException(); }
77+
@Override public void setEntity(Object entity) { throw new UnsupportedOperationException(); }
78+
@Override public void setEntity(Object entity, Annotation[] annotations, MediaType mediaType) { throw new UnsupportedOperationException(); }
79+
@Override public Annotation[] getEntityAnnotations() { throw new UnsupportedOperationException(); }
80+
@Override public OutputStream getEntityStream() { throw new UnsupportedOperationException(); }
81+
@Override public void setEntityStream(OutputStream outputStream) { throw new UnsupportedOperationException(); }
82+
@Override public Client getClient() { throw new UnsupportedOperationException(); }
83+
@Override public Configuration getConfiguration() { throw new UnsupportedOperationException(); }
84+
@Override public void abortWith(Response response) { throw new UnsupportedOperationException(); }
85+
}
86+
87+
/** Non-matching host: filter must leave the URI untouched. */
88+
@Test
89+
public void testNoRewriteForNonMatchingHost() throws IOException
90+
{
91+
ClientUriRewriteFilter filter = new ClientUriRewriteFilter("example.com", "http", "nginx", 9443);
92+
StubRequestContext ctx = new StubRequestContext(URI.create("https://other.org/path"));
93+
filter.filter(ctx);
94+
assertEquals(URI.create("https://other.org/path"), ctx.getUri());
95+
assertTrue(ctx.getHeaders().isEmpty());
96+
}
97+
98+
/** Exact host match: URI host is rewritten to proxyHost, scheme to proxyScheme. */
99+
@Test
100+
public void testRewriteExactHost() throws IOException
101+
{
102+
ClientUriRewriteFilter filter = new ClientUriRewriteFilter("example.com", "http", "nginx", 9443);
103+
StubRequestContext ctx = new StubRequestContext(URI.create("https://example.com/path?q=1"));
104+
filter.filter(ctx);
105+
assertEquals(URI.create("http://nginx:9443/path?q=1"), ctx.getUri());
106+
assertEquals("example.com", ctx.getHeaders().getFirst("Host"));
107+
}
108+
109+
/** Exact host match with explicit port: Host header must include the original port. */
110+
@Test
111+
public void testRewriteExactHostWithPort() throws IOException
112+
{
113+
ClientUriRewriteFilter filter = new ClientUriRewriteFilter("example.com", "http", "nginx", 9443);
114+
StubRequestContext ctx = new StubRequestContext(URI.create("https://example.com:4443/path"));
115+
filter.filter(ctx);
116+
assertEquals(URI.create("http://nginx:9443/path"), ctx.getUri());
117+
assertEquals("example.com:4443", ctx.getHeaders().getFirst("Host"));
118+
}
119+
120+
/**
121+
* Subdomain match with same-domain proxy host (production setup):
122+
* subdomain prefix must be preserved in the rewritten URI so the HTTP client
123+
* does not reuse a connection established with a different TLS SNI, which
124+
* would cause nginx to return 421 Misdirected Request.
125+
*/
126+
@Test
127+
public void testRewriteSubdomainPreservesSubdomainWithSameDomainProxy() throws IOException
128+
{
129+
ClientUriRewriteFilter filter = new ClientUriRewriteFilter("example.com", "https", "example.com", 5443);
130+
StubRequestContext ctx = new StubRequestContext(URI.create("https://admin.example.com/acl/agents/123/"));
131+
filter.filter(ctx);
132+
assertEquals(URI.create("https://admin.example.com:5443/acl/agents/123/"), ctx.getUri());
133+
assertEquals("admin.example.com", ctx.getHeaders().getFirst("Host"));
134+
}
135+
136+
/**
137+
* Subdomain match with internal proxy host (Docker Compose setup):
138+
* subdomain prefix must NOT be prepended to the proxy hostname — the internal
139+
* hostname (e.g. "nginx") has no subdomain equivalent. nginx routes via Host header.
140+
*/
141+
@Test
142+
public void testRewriteSubdomainWithInternalProxyUsesProxyHostOnly() throws IOException
143+
{
144+
ClientUriRewriteFilter filter = new ClientUriRewriteFilter("example.com", "http", "nginx", 9443);
145+
StubRequestContext ctx = new StubRequestContext(URI.create("https://admin.example.com/path"));
146+
filter.filter(ctx);
147+
assertEquals(URI.create("http://nginx:9443/path"), ctx.getUri());
148+
assertEquals("admin.example.com", ctx.getHeaders().getFirst("Host"));
149+
}
150+
151+
/** Query string with special characters must survive URI rewrite without decoding. */
152+
@Test
153+
public void testQueryStringNotDecoded() throws IOException
154+
{
155+
ClientUriRewriteFilter filter = new ClientUriRewriteFilter("example.com", "http", "nginx", 9443);
156+
StubRequestContext ctx = new StubRequestContext(URI.create("https://example.com/sparql?query=ASK+%7B%7D"));
157+
filter.filter(ctx);
158+
assertEquals("query=ASK+%7B%7D", ctx.getUri().getRawQuery());
159+
assertEquals("example.com", ctx.getHeaders().getFirst("Host"));
160+
}
161+
162+
}

0 commit comments

Comments
 (0)