Skip to content

Commit f7ee4bd

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

File tree

2 files changed

+609
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)