Skip to content

Commit d74bb39

Browse files
committed
fix(transport): add explicit trusted local endpoint policy
1 parent 441bdd4 commit d74bb39

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package io.github.easy4j.hermes.security;
2+
3+
import io.github.easy4j.hermes.util.EndpointGuard;
4+
5+
import java.net.InetAddress;
6+
import java.net.URI;
7+
import java.net.URISyntaxException;
8+
import java.net.UnknownHostException;
9+
import java.util.Locale;
10+
import java.util.Objects;
11+
12+
/**
13+
* Explicit outbound endpoint policy for Hermes HTTP and SSE transports.
14+
*
15+
* <p>The default policy preserves the historical strict public-endpoint
16+
* validation. Trusted local access is opt-in and is constrained to one
17+
* loopback host and one explicit port.</p>
18+
*
19+
* @since 1.0.0
20+
*/
21+
public final class EndpointPolicy {
22+
23+
private enum Mode {
24+
STRICT_PUBLIC,
25+
TRUSTED_LOCAL
26+
}
27+
28+
private final Mode mode;
29+
private final String trustedHost;
30+
private final int trustedPort;
31+
32+
private EndpointPolicy(Mode mode, String trustedHost, int trustedPort) {
33+
this.mode = Objects.requireNonNull(mode, "mode");
34+
this.trustedHost = trustedHost;
35+
this.trustedPort = trustedPort;
36+
}
37+
38+
public static EndpointPolicy strictPublic() {
39+
return new EndpointPolicy(Mode.STRICT_PUBLIC, null, -1);
40+
}
41+
42+
public static EndpointPolicy trustedLocal(String host, int port) {
43+
String normalized = normalizeHost(host);
44+
if (port < 1 || port > 65535) {
45+
throw new IllegalArgumentException("Trusted local port must be between 1 and 65535");
46+
}
47+
InetAddress[] addresses = resolveAll(normalized);
48+
if (addresses.length == 0) {
49+
throw new IllegalArgumentException("Trusted local host did not resolve: " + host);
50+
}
51+
for (InetAddress address : addresses) {
52+
if (!address.isLoopbackAddress()) {
53+
throw new IllegalArgumentException(
54+
"Trusted local host must resolve only to loopback addresses: " + host);
55+
}
56+
}
57+
return new EndpointPolicy(Mode.TRUSTED_LOCAL, normalized, port);
58+
}
59+
60+
public String require(String url) {
61+
if (mode == Mode.STRICT_PUBLIC) {
62+
return EndpointGuard.require(url);
63+
}
64+
URI uri = parse(url);
65+
requireHttpScheme(uri, url);
66+
String host = normalizeHost(uri.getHost());
67+
int port = effectivePort(uri);
68+
if (!trustedHost.equals(host) || trustedPort != port) {
69+
throw new IllegalArgumentException(
70+
"Endpoint is outside the trusted local origin: " + url);
71+
}
72+
InetAddress[] addresses = resolveAll(host);
73+
for (InetAddress address : addresses) {
74+
if (!address.isLoopbackAddress()) {
75+
throw new IllegalArgumentException(
76+
"Trusted local endpoint resolved outside loopback: " + url);
77+
}
78+
}
79+
return url;
80+
}
81+
82+
private static URI parse(String url) {
83+
Objects.requireNonNull(url, "url");
84+
try {
85+
return new URI(url);
86+
} catch (URISyntaxException error) {
87+
throw new IllegalArgumentException("Malformed URL: " + url, error);
88+
}
89+
}
90+
91+
private static void requireHttpScheme(URI uri, String url) {
92+
String scheme = uri.getScheme();
93+
if (scheme == null
94+
|| (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
95+
throw new IllegalArgumentException("Endpoint URL must use http or https: " + url);
96+
}
97+
if (uri.getHost() == null || uri.getHost().isEmpty()) {
98+
throw new IllegalArgumentException("Endpoint URL must have a host: " + url);
99+
}
100+
}
101+
102+
private static int effectivePort(URI uri) {
103+
if (uri.getPort() >= 0) {
104+
return uri.getPort();
105+
}
106+
return "https".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
107+
}
108+
109+
private static String normalizeHost(String host) {
110+
if (host == null || host.trim().isEmpty()) {
111+
throw new IllegalArgumentException("Endpoint host must not be blank");
112+
}
113+
String value = host.trim();
114+
if (value.length() >= 2 && value.charAt(0) == '['
115+
&& value.charAt(value.length() - 1) == ']') {
116+
value = value.substring(1, value.length() - 1);
117+
}
118+
return value.toLowerCase(Locale.ROOT);
119+
}
120+
121+
private static InetAddress[] resolveAll(String host) {
122+
try {
123+
return InetAddress.getAllByName(host);
124+
} catch (UnknownHostException error) {
125+
throw new IllegalArgumentException("Endpoint host could not be resolved: " + host, error);
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)