Skip to content

Commit 44ae3be

Browse files
Transparent proxy: TP Loader Cloud SDK native integration
1 parent 9d44dd4 commit 44ae3be

File tree

3 files changed

+866
-0
lines changed

3 files changed

+866
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.UnknownHostException;
7+
8+
import javax.annotation.Nonnull;
9+
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
14+
15+
/**
16+
* NetworkVerifier that performs TCP socket-based connectivity verification.
17+
*
18+
* <p>
19+
* This implementation uses standard Java socket connections to verify that a remote host and port combination is
20+
* reachable and accepting connections. It establishes a temporary TCP connection to the target endpoint and immediately
21+
* closes it upon successful establishment.
22+
*
23+
* <p>
24+
* <strong>Verification Process:</strong>
25+
* <ol>
26+
* <li>Creates a new TCP socket</li>
27+
* <li>Attempts to connect to the specified host and port with a timeout</li>
28+
* <li>Immediately closes the connection if successful</li>
29+
* <li>Throws appropriate exceptions for failures</li>
30+
* </ol>
31+
*
32+
* <p>
33+
* <strong>Timeout Configuration:</strong> The verification uses a fixed timeout of {@value #HOST_REACH_TIMEOUT}
34+
* milliseconds to prevent indefinite blocking on unreachable endpoints.
35+
*
36+
* <p>
37+
* <strong>Error Handling:</strong>
38+
* <ul>
39+
* <li>{@link UnknownHostException} - Host cannot be resolved to an IP address</li>
40+
* <li>{@link IOException} - Network connectivity issues or connection refused</li>
41+
* </ul>
42+
*
43+
* @see TransparentProxy
44+
* @since 5.24.0
45+
*/
46+
class NetworkVerifier
47+
{
48+
private static final int HOST_REACH_TIMEOUT = 5000;
49+
private static final Logger log = LoggerFactory.getLogger(NetworkVerifier.class);
50+
51+
/**
52+
* {@inheritDoc}
53+
*
54+
* <p>
55+
* This implementation creates a TCP socket connection to the specified host and port to verify connectivity. The
56+
* connection is immediately closed after successful establishment.
57+
*
58+
* @param host
59+
* {@inheritDoc}
60+
* @param port
61+
* {@inheritDoc}
62+
* @throws DestinationAccessException
63+
* {@inheritDoc}
64+
* <p>
65+
* Specific error conditions:
66+
* <ul>
67+
* <li>Host resolution failure - when DNS lookup fails</li>
68+
* <li>Connection failure - when host is unreachable or port is closed</li>
69+
* <li>Network timeouts - when connection attempt exceeds timeout</li>
70+
* </ul>
71+
*/
72+
void verifyHostConnectivity( @Nonnull final String host, final int port )
73+
throws DestinationAccessException
74+
{
75+
log.info("Verifying that transparent proxy host is reachable on {}:{}", host, port);
76+
try( Socket socket = new Socket() ) {
77+
socket.connect(new InetSocketAddress(host, port), HOST_REACH_TIMEOUT);
78+
log
79+
.info(
80+
"Successfully verified successfully that transparent proxy host is reachable on {}:{}",
81+
host,
82+
port);
83+
}
84+
catch( final UnknownHostException e ) {
85+
throw new DestinationAccessException(
86+
String.format("Host [%s] could not be resolved. Caused by: %s", host, e.getMessage()),
87+
e);
88+
}
89+
catch( final IOException e ) {
90+
throw new DestinationAccessException(
91+
String.format("Host [%s] on port [%d] is not reachable. Caused by: %s", host, port, e.getMessage()),
92+
e);
93+
}
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package com.sap.cloud.sdk.cloudplatform.connectivity;
2+
3+
import java.io.IOException;
4+
import java.net.URI;
5+
import java.net.URISyntaxException;
6+
7+
import javax.annotation.Nonnull;
8+
9+
import org.apache.http.HttpMessage;
10+
import org.apache.http.HttpResponse;
11+
import org.apache.http.HttpStatus;
12+
import org.apache.http.client.HttpClient;
13+
import org.apache.http.client.methods.HttpHead;
14+
15+
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
16+
17+
import io.vavr.control.Try;
18+
import lombok.extern.slf4j.Slf4j;
19+
20+
/**
21+
* A transparent proxy loader that enables routing traffic through a single registered gateway host.
22+
*
23+
* <p>
24+
* This class provides a mechanism to register a proxy gateway that will handle all destination requests transparently.
25+
* Once registered, all destination lookups will be routed through the configured gateway host and port.
26+
*
27+
* <p>
28+
* <strong>Key Features:</strong>
29+
* <ul>
30+
* <li>Single gateway registration - only one proxy can be registered at a time</li>
31+
* <li>Host validation - ensures hosts don't contain paths and are reachable</li>
32+
* <li>Automatic scheme normalization - defaults to HTTP if no scheme provided</li>
33+
* <li>Network connectivity validation before registration</li>
34+
* </ul>
35+
*
36+
* <p>
37+
* <strong>Usage Example:</strong>
38+
*
39+
* <pre>{@code
40+
* // Register with default port 80
41+
* TransparentProxy.register("gateway.svc.cluster.local");
42+
*
43+
* // Register with custom port
44+
* TransparentProxy.register("http://gateway.svc.cluster.local", 8080);
45+
* }</pre>
46+
*
47+
* <p>
48+
* <strong>Thread Safety:</strong> This class uses static state and is not thread-safe. Registration should be performed
49+
* during application initialization.
50+
*
51+
* @since 5.24.0
52+
*/
53+
@Slf4j
54+
public class TransparentProxy implements DestinationLoader
55+
{
56+
private static final String X_ERROR_INTERNAL_CODE_HEADER = "x-error-internal-code";
57+
private static final Integer DEFAULT_PORT = 80;
58+
private static final String SCHEME_SEPARATOR = "://";
59+
private static final String HTTP_SCHEME = org.apache.http.HttpHost.DEFAULT_SCHEME_NAME + SCHEME_SEPARATOR;
60+
private static final String PORT_SEPARATOR = ":";
61+
private static final String HOST_CONTAINS_PATH_ERROR_MESSAGE_TEMPLATE =
62+
"Host '%s' contains a path '%s'. Paths are not allowed in host registration.";
63+
static String uri;
64+
static NetworkVerifier networkVerifier = new NetworkVerifier();
65+
66+
/**
67+
* Registers a transparent proxy gateway using the default port 80.
68+
*
69+
* <p>
70+
* This method registers the specified host as a transparent proxy gateway that will handle all subsequent
71+
* destination requests. The host will be validated for reachability and must not contain any path components.
72+
*
73+
* <p>
74+
* If no scheme is provided, HTTP will be used by default. The final URI will be constructed as:
75+
* {@code <normalized-host>:80}
76+
*
77+
* @param host
78+
* the gateway host to register (e.g., "gateway.svc.cluster.local") Must not contain paths or be null
79+
* @throws DestinationAccessException
80+
* if the proxy is already registered, the host contains a path, or the host is not reachable on port 80
81+
* @throws IllegalArgumentException
82+
* if host is null
83+
* @see #register(String, Integer)
84+
*/
85+
public static void register( @Nonnull final String host )
86+
{
87+
registerLoader(host, DEFAULT_PORT);
88+
}
89+
90+
/**
91+
* Registers a transparent proxy gateway with a specified port.
92+
*
93+
* <p>
94+
* This method registers the specified host and port as a transparent proxy gateway that will handle all subsequent
95+
* destination requests. The host will be validated for reachability on the specified port and must not contain any
96+
* path components.
97+
*
98+
* <p>
99+
* If no scheme is provided, HTTP will be used by default. The final URI will be constructed as:
100+
* {@code <normalized-host>:<port>}
101+
*
102+
* @param host
103+
* the gateway host to register (e.g., "gateway" or "<a href="http://gateway">...</a>") Must not contain
104+
* paths or be null
105+
* @param port
106+
* the port number to use for the gateway connection. Must not be null and should be a valid port number
107+
* (1-65535)
108+
* @throws DestinationAccessException
109+
* if the proxy is already registered, the host contains a path, or the host is not reachable on the
110+
* specified port
111+
* @throws IllegalArgumentException
112+
* if host or port is null
113+
* @see #register(String)
114+
*/
115+
public static void register( @Nonnull final String host, @Nonnull final Integer port )
116+
{
117+
registerLoader(host, port);
118+
}
119+
120+
private static void registerLoader( @Nonnull final String host, final Integer port )
121+
{
122+
if( uri != null ) {
123+
throw new DestinationAccessException(
124+
"TransparentProxy is already registered. Only one registration is allowed.");
125+
}
126+
127+
try {
128+
final String normalizedHost = normalizeHostWithScheme(host);
129+
final String hostForVerification = getHostForVerification(host, normalizedHost);
130+
131+
verifyHostConnectivity(hostForVerification, port);
132+
133+
uri = String.format("%s%s%d", normalizedHost, PORT_SEPARATOR, port);
134+
DestinationAccessor.prependDestinationLoader(new TransparentProxy());
135+
136+
}
137+
catch( final URISyntaxException e ) {
138+
throw new DestinationAccessException(
139+
String.format("Invalid host format: [%s]. Caused by: %s", host, e.getMessage()),
140+
e);
141+
}
142+
}
143+
144+
@Nonnull
145+
private static String getHostForVerification( @Nonnull final String host, final String normalizedHost )
146+
throws URISyntaxException
147+
{
148+
final URI parsedUri = new URI(normalizedHost);
149+
150+
final String path = parsedUri.getPath();
151+
if( path != null && !path.isEmpty() ) {
152+
throw new DestinationAccessException(String.format(HOST_CONTAINS_PATH_ERROR_MESSAGE_TEMPLATE, host, path));
153+
}
154+
155+
final String hostForVerification = parsedUri.getHost();
156+
if( hostForVerification == null ) {
157+
throw new DestinationAccessException(String.format("Invalid host format: [%s]", host));
158+
}
159+
return hostForVerification;
160+
}
161+
162+
@Nonnull
163+
private static String normalizeHostWithScheme( @Nonnull final String host )
164+
{
165+
if( host.contains(SCHEME_SEPARATOR) ) {
166+
return host;
167+
}
168+
return HTTP_SCHEME + host;
169+
}
170+
171+
private static void verifyHostConnectivity( @Nonnull final String host, final int port )
172+
{
173+
networkVerifier.verifyHostConnectivity(host, port);
174+
}
175+
176+
/**
177+
* Verifies if the destination is found by making a HEAD HTTP request.
178+
*
179+
* @param destination
180+
* the destination to use
181+
* @param destinationName
182+
* the name of the destination to check
183+
* @return true if the destination is not found, false otherwise
184+
*/
185+
private static
186+
boolean
187+
isDestinationNotFound( @Nonnull final TransparentProxyDestination destination, final String destinationName )
188+
{
189+
final HttpClient httpClient = HttpClientAccessor.getHttpClient(destination);
190+
final URI destinationUri = destination.getUri();
191+
final HttpHead headRequest = new HttpHead(destinationUri);
192+
log.debug("Making HEAD request to check if destination {} is found...", destinationName);
193+
final HttpResponse response;
194+
try {
195+
response = httpClient.execute(headRequest);
196+
}
197+
catch( IOException e ) {
198+
log.debug("HEAD request to destination {} failed with exception: {}", destinationName, e.getMessage(), e);
199+
return false;
200+
}
201+
final int statusCode = response.getStatusLine().getStatusCode();
202+
203+
boolean destinationNotFound = false;
204+
if( statusCode == HttpStatus.SC_BAD_GATEWAY ) {
205+
final String errorInternalCode = getHeaderValue(response, X_ERROR_INTERNAL_CODE_HEADER);
206+
if( Integer.toString(HttpStatus.SC_NOT_FOUND).equals(errorInternalCode) ) {
207+
destinationNotFound = true;
208+
}
209+
}
210+
211+
log
212+
.debug(
213+
"HEAD request to destination {} returned status code: {}, x-error-internal-code: {}, found: {}",
214+
destinationName,
215+
statusCode,
216+
getHeaderValue(response, X_ERROR_INTERNAL_CODE_HEADER),
217+
!destinationNotFound);
218+
219+
return destinationNotFound;
220+
}
221+
222+
/**
223+
* Helper method to extract header value from HTTP message.
224+
*
225+
* @param message
226+
* the HTTP message
227+
* @param headerName
228+
* the name of the header to extract
229+
* @return the header value if present, "" otherwise
230+
*/
231+
private static String getHeaderValue( @Nonnull final HttpMessage message, @Nonnull final String headerName )
232+
{
233+
if( message.containsHeader(headerName) ) {
234+
return message.getFirstHeader(headerName).getValue();
235+
}
236+
return "";
237+
}
238+
239+
@Nonnull
240+
@Override
241+
public Try<Destination> tryGetDestination( @Nonnull final String destinationName )
242+
{
243+
final TransparentProxyDestination destination =
244+
TransparentProxyDestination.gateway(destinationName, uri).build();
245+
246+
if( isDestinationNotFound(destination, destinationName) ) {
247+
return Try.failure(new DestinationAccessException("Destination not found: " + destinationName));
248+
}
249+
250+
return Try.success(destination);
251+
}
252+
253+
@Nonnull
254+
@Override
255+
public
256+
Try<Destination>
257+
tryGetDestination( @Nonnull final String destinationName, @Nonnull DestinationOptions options )
258+
{
259+
final TransparentProxyDestination destination =
260+
TransparentProxyDestination.gateway(destinationName, uri).build();
261+
262+
if( isDestinationNotFound(destination, destinationName) ) {
263+
return Try.failure(new DestinationAccessException("Destination not found: " + destinationName));
264+
}
265+
266+
return Try.success(destination);
267+
}
268+
269+
}

0 commit comments

Comments
 (0)