Skip to content

Commit bc1d50c

Browse files
Transparent proxy: TP Loader Cloud SDK native integration
1 parent 48616ac commit bc1d50c

File tree

2 files changed

+601
-0
lines changed

2 files changed

+601
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package com.sap.cloud.sdk.cloudplatform.connectivity;
2+
3+
import java.io.IOException;
4+
import java.net.*;
5+
6+
import javax.annotation.Nonnull;
7+
8+
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
9+
10+
import io.vavr.control.Try;
11+
12+
/**
13+
* A transparent proxy loader that enables routing traffic through a single registered gateway host.
14+
*
15+
* <p>
16+
* This class provides a mechanism to register a proxy gateway that will handle all destination requests transparently.
17+
* Once registered, all destination lookups will be routed through the configured gateway host and port.
18+
*
19+
* <p>
20+
* <strong>Key Features:</strong>
21+
* <ul>
22+
* <li>Single gateway registration - only one proxy can be registered at a time</li>
23+
* <li>Host validation - ensures hosts don't contain paths and are reachable</li>
24+
* <li>Automatic scheme normalization - defaults to HTTP if no scheme provided</li>
25+
* <li>Network connectivity validation before registration</li>
26+
* </ul>
27+
*
28+
* <p>
29+
* <strong>Usage Example:</strong>
30+
*
31+
* <pre>{@code
32+
* // Register with default port 80
33+
* TransparentProxy.register("gateway.svc.cluster.local");
34+
*
35+
* // Register with custom port
36+
* TransparentProxy.register("http://gateway.svc.cluster.local", 8080);
37+
* }</pre>
38+
*
39+
* <p>
40+
* <strong>Thread Safety:</strong> This class uses static state and is not thread-safe. Registration should be performed
41+
* during application initialization.
42+
*
43+
* @since 5.24.0
44+
*/
45+
public class TransparentProxy implements DestinationLoader
46+
{
47+
private static final Integer DEFAULT_PORT = 80;
48+
private static final String SCHEME_SEPARATOR = "://";
49+
private static final String HTTP_SCHEME = org.apache.http.HttpHost.DEFAULT_SCHEME_NAME + SCHEME_SEPARATOR;
50+
private static final String PATH_SEPARATOR = "/";
51+
private static final String PORT_SEPARATOR = ":";
52+
private static final String HOST_CONTAINS_PATH_ERROR_MESSAGE_TEMPLATE =
53+
"Host '%s' contains a path '%s'. Paths are not allowed in host registration.";
54+
static String uri;
55+
static NetworkValidator networkValidator = new DefaultNetworkValidator();
56+
57+
/**
58+
* Registers a transparent proxy gateway using the default port 80.
59+
*
60+
* <p>
61+
* This method registers the specified host as a transparent proxy gateway that will handle all subsequent
62+
* destination requests. The host will be validated for reachability and must not contain any path components.
63+
*
64+
* <p>
65+
* If no scheme is provided, HTTP will be used by default. The final URI will be constructed as:
66+
* {@code <normalized-host>:80}
67+
*
68+
* @param host
69+
* the gateway host to register (e.g., "gateway.svc.cluster.local") Must not contain paths or be null
70+
* @throws DestinationAccessException
71+
* if the proxy is already registered, the host contains a path, or the host is not reachable on port 80
72+
* @throws IllegalArgumentException
73+
* if host is null
74+
* @see #register(String, Integer)
75+
*/
76+
public static void register( @Nonnull final String host )
77+
{
78+
registerLoader(host, DEFAULT_PORT);
79+
}
80+
81+
/**
82+
* Registers a transparent proxy gateway with a specified port.
83+
*
84+
* <p>
85+
* This method registers the specified host and port as a transparent proxy gateway that will handle all subsequent
86+
* destination requests. The host will be validated for reachability on the specified port and must not contain any
87+
* path components.
88+
*
89+
* <p>
90+
* If no scheme is provided, HTTP will be used by default. The final URI will be constructed as:
91+
* {@code <normalized-host>:<port>}
92+
*
93+
* @param host
94+
* the gateway host to register (e.g., "gateway" or "http://gateway") Must not contain paths or be null
95+
* @param port
96+
* the port number to use for the gateway connection. Must not be null and should be a valid port number
97+
* (1-65535)
98+
* @throws DestinationAccessException
99+
* if the proxy is already registered, the host contains a path, or the host is not reachable on the
100+
* specified port
101+
* @throws IllegalArgumentException
102+
* if host or port is null
103+
* @see #register(String)
104+
*/
105+
public static void register( @Nonnull final String host, @Nonnull final Integer port )
106+
{
107+
registerLoader(host, port);
108+
}
109+
110+
private static void registerLoader( @Nonnull final String host, final Integer port )
111+
{
112+
if( uri != null ) {
113+
throw new DestinationAccessException(
114+
"TransparentProxy is already registered. Only one registration is allowed.");
115+
}
116+
117+
validateHostHasNoPath(host);
118+
119+
final String normalizedHost = normalizeHostWithScheme(host);
120+
final String hostForValidation = extractHostForValidation(normalizedHost);
121+
122+
validateHostConnectivity(hostForValidation, port);
123+
124+
TransparentProxy.uri = String.format("%s%s%d", normalizedHost, PORT_SEPARATOR, port);
125+
DestinationAccessor.prependDestinationLoader(new TransparentProxy());
126+
}
127+
128+
@Nonnull
129+
private static String normalizeHostWithScheme( @Nonnull final String host )
130+
{
131+
if( host.contains(SCHEME_SEPARATOR) ) {
132+
return host;
133+
}
134+
return HTTP_SCHEME + host;
135+
}
136+
137+
private static String extractHostForValidation( @Nonnull final String normalizedHost )
138+
{
139+
try {
140+
final URI uri = new URI(normalizedHost);
141+
return uri.getHost();
142+
}
143+
catch( final URISyntaxException e ) {
144+
String host = normalizedHost;
145+
if( host.contains(SCHEME_SEPARATOR) ) {
146+
host = host.substring(host.indexOf(SCHEME_SEPARATOR) + 3);
147+
}
148+
if( host.contains(PORT_SEPARATOR) ) {
149+
host = host.substring(0, host.indexOf(PORT_SEPARATOR));
150+
}
151+
return host;
152+
}
153+
}
154+
155+
private static void validateHostConnectivity( @Nonnull final String host, final int port )
156+
{
157+
networkValidator.validateHostConnectivity(host, port);
158+
}
159+
160+
private static void validateHostHasNoPath( @Nonnull final String host )
161+
{
162+
try {
163+
final URI uri = new URI(host.contains(SCHEME_SEPARATOR) ? host : HTTP_SCHEME + host);
164+
final String path = uri.getPath();
165+
if( path != null && !path.isEmpty() ) {
166+
throw new DestinationAccessException(
167+
String.format(HOST_CONTAINS_PATH_ERROR_MESSAGE_TEMPLATE, host, path));
168+
}
169+
}
170+
catch( final URISyntaxException e ) {
171+
final String hostToCheck =
172+
host.contains(SCHEME_SEPARATOR) ? host.substring(host.indexOf(SCHEME_SEPARATOR) + 3) : host;
173+
if( hostToCheck.contains(PATH_SEPARATOR) ) {
174+
final String pathPart = hostToCheck.substring(hostToCheck.indexOf(PATH_SEPARATOR));
175+
throw new DestinationAccessException(
176+
String.format(HOST_CONTAINS_PATH_ERROR_MESSAGE_TEMPLATE, host, pathPart));
177+
}
178+
}
179+
}
180+
181+
@Nonnull
182+
@Override
183+
public Try<Destination> tryGetDestination( @Nonnull String destinationName )
184+
{
185+
return Try.success(TransparentProxyDestination.gateway(destinationName, uri).build());
186+
}
187+
188+
@Nonnull
189+
@Override
190+
public Try<Destination> tryGetDestination( @Nonnull String destinationName, @Nonnull DestinationOptions options )
191+
{
192+
return Try.success(TransparentProxyDestination.gateway(destinationName, uri).build());
193+
}
194+
195+
interface NetworkValidator
196+
{
197+
void validateHostConnectivity( @Nonnull String host, int port )
198+
throws DestinationAccessException;
199+
}
200+
201+
static class DefaultNetworkValidator implements NetworkValidator
202+
{
203+
private static final int HOST_REACH_TIMEOUT = 5000;
204+
205+
@Override
206+
public void validateHostConnectivity( @Nonnull final String host, final int port )
207+
throws DestinationAccessException
208+
{
209+
try( Socket socket = new Socket() ) {
210+
socket.connect(new InetSocketAddress(host, port), HOST_REACH_TIMEOUT);
211+
}
212+
catch( final UnknownHostException e ) {
213+
throw new DestinationAccessException(String.format("Host [%s] could not be resolved", host), e);
214+
}
215+
catch( final IOException e ) {
216+
throw new DestinationAccessException(
217+
String.format("Host [%s] on port [%d] is not reachable", host, port),
218+
e);
219+
}
220+
}
221+
}
222+
223+
}

0 commit comments

Comments
 (0)