Skip to content

Commit

Permalink
Use interceptor approach instead of direct proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
byronantak committed Dec 2, 2024
1 parent 2c89e8d commit 70730b2
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -52,29 +51,28 @@ public class AuthenticatedRestTemplate {

@Autowired ResttemplateConfig restTemplateConfig;

@Autowired ProxyCheckService proxyCheckService;

/** Will delegate calls to the {@link RestTemplate} instance that was configured */
@Autowired ProxiedCookieStoreRestTemplate restTemplate;
@Autowired CookieStoreRestTemplate restTemplate;

/** Used to intercept requests and inject CSRF token */
@Autowired LoginAuthenticationCsrfTokenInterceptor loginAuthenticationCsrfTokenInterceptor;

@Autowired RestTemplateUtil restTemplateUtil;

@Autowired ProxyOutboundRequestInterceptor proxyOutboundRequestInterceptor;

/** Initialize the internal restTemplate instance */
@PostConstruct
protected void init() {
logger.debug("Create the RestTemplate instance that will be wrapped");

configureEnvoyProxy(restTemplate);
makeRestTemplateWithCustomObjectMapper(restTemplate);
setErrorHandlerWithLogging(restTemplate);

logger.debug("Set interceptor for authentication");
List<ClientHttpRequestInterceptor> interceptors =
Collections.<ClientHttpRequestInterceptor>singletonList(
loginAuthenticationCsrfTokenInterceptor);
Collections.<ClientHttpRequestInterceptor>unmodifiableList(
List.of(proxyOutboundRequestInterceptor, loginAuthenticationCsrfTokenInterceptor));

restTemplate.setRequestFactory(
new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors));
Expand Down Expand Up @@ -147,42 +145,6 @@ protected void makeRestTemplateWithCustomObjectMapper(RestTemplate restTemplate)
}
}

/***
* Send a dummy request to the proxy. If we get a success, it's configured
*/
protected boolean isProxyConfigured() {
String testUrl =
UriComponentsBuilder.newInstance()
.scheme("http")
.host(restTemplateConfig.getProxyHost())
.port(restTemplateConfig.getProxyPort())
.path("login")
.build()
.toUriString();
HttpHeaders headers = new HttpHeaders();
headers.set("Host", restTemplateConfig.getHost());
HttpEntity<String> httpEntity = new HttpEntity<>(null, headers);
ResponseEntity<Void> response =
restTemplate.exchange(testUrl, HttpMethod.GET, httpEntity, Void.class);
return response.getStatusCode().is2xxSuccessful();
}

/***
* Update RestTemplate to use Proxy for all requests going forward;
* adds headers to indicate the host
*/
protected void configureEnvoyProxy(ProxiedCookieStoreRestTemplate restTemplate) {
boolean isEnvoyConfigured = proxyCheckService.hasProxy();
if (isEnvoyConfigured) {
restTemplate.configureProxy(
true,
restTemplateConfig.getProxyHost(),
restTemplateConfig.getProxyPort(),
(httpRequest, entityDetails, httpContext) ->
httpRequest.addHeader("Host", restTemplateConfig.getHost()));
}
}

/**
* Gets the full URI (scheme + host + port + path) to access the REST WS for a given resource
* path.
Expand Down Expand Up @@ -395,7 +357,7 @@ public void put(String resourcePath, Object request) {
}

@VisibleForTesting
public ProxiedCookieStoreRestTemplate getRestTemplate() {
public CookieStoreRestTemplate getRestTemplate() {
return restTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
package com.box.l10n.mojito.rest.resttemplate;

import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
Expand All @@ -20,11 +15,11 @@
* @author wyau
*/
@Component
public class ProxiedCookieStoreRestTemplate extends RestTemplate {
public class CookieStoreRestTemplate extends RestTemplate {

CookieStore cookieStore;

public ProxiedCookieStoreRestTemplate() {
public CookieStoreRestTemplate() {
super();
cookieStore = new BasicCookieStore();
setCookieStoreAndUpdateRequestFactory(cookieStore);
Expand All @@ -45,28 +40,6 @@ public void setCookieStoreAndUpdateRequestFactory(CookieStore cookieStore) {
setRequestFactory(new HttpComponentsClientHttpRequestFactory(hc));
}

public void configureProxy(
boolean useProxy, String proxyHost, int proxyPort, HttpRequestInterceptor proxyInterceptor) {
logger.debug("Configuring proxy for Rest Template. Enabled = {}", useProxy);
HttpClientBuilder clientBuilder =
HttpClients.custom().setDefaultCookieStore(cookieStore).disableRedirectHandling();

if (useProxy) {
logger.debug("Configuring proxy for Rest Template. Proxy settings {}:{}", proxyHost, proxyPort);
if (proxyInterceptor != null) {
HttpHost proxy = new HttpHost(proxyHost, proxyPort);
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
RequestConfig config = RequestConfig.custom().build();
clientBuilder.setDefaultRequestConfig(config);
clientBuilder.addRequestInterceptorFirst(proxyInterceptor);
clientBuilder.setRoutePlanner(routePlanner);
}
}

// Rebuild the request factory with or without proxy settings
setRequestFactory(new HttpComponentsClientHttpRequestFactory(clientBuilder.build()));
}

public CookieStore getCookieStore() {
return cookieStore;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -57,7 +56,7 @@ public class LoginAuthenticationCsrfTokenInterceptor implements ClientHttpReques
* This is used for the authentication flow to keep things separate from the restTemplate that
* this interceptor is intercepting
*/
ProxiedCookieStoreRestTemplate restTemplateForAuthenticationFlow;
CookieStoreRestTemplate restTemplateForAuthenticationFlow;

/**
* This is so that we obtain access to the cookie store used inside HttpClient to check to see if
Expand All @@ -77,31 +76,24 @@ public class LoginAuthenticationCsrfTokenInterceptor implements ClientHttpReques
@Autowired CredentialProvider credentialProvider;

/** Will delegate calls to the {@link RestTemplate} instance that was configured */
@Autowired ProxiedCookieStoreRestTemplate restTemplate;
@Autowired CookieStoreRestTemplate restTemplate;

@Autowired ProxyOutboundRequestInterceptor proxyOutboundRequestInterceptor;

/** Init */
@PostConstruct
protected void init() {

restTemplateForAuthenticationFlow = new ProxiedCookieStoreRestTemplate();
restTemplateForAuthenticationFlow = new CookieStoreRestTemplate();
cookieStore = restTemplateForAuthenticationFlow.getCookieStore();
boolean hasProxy = proxyCheckService.hasProxy();
if (hasProxy) {
logger.debug("Configuring Proxy for authenticationRestTemplate");
restTemplateForAuthenticationFlow.configureProxy(
true,
resttemplateConfig.getProxyHost(),
resttemplateConfig.getProxyPort(),
(httpRequest, entityDetails, httpContext) ->
httpRequest.addHeader("Host", resttemplateConfig.getHost()));
}

logger.debug(
"Inject cookie store used in the rest template for authentication flow into the restTemplate so that they will match");
restTemplate.setCookieStoreAndUpdateRequestFactory(cookieStore);

List<ClientHttpRequestInterceptor> interceptors =
Collections.singletonList(
List.of(
proxyOutboundRequestInterceptor,
new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(
Expand Down Expand Up @@ -152,8 +144,8 @@ public ClientHttpResponse intercept(

/**
* Handle http response from the intercept. It will check to see if the initial response was
* successful (ie. error status such as 301, 403). If so, it'll try the authentication flow again.
* If it further encounters an unsuccessful response, then it'll throw a {@link
* successful (i.e. error status such as 301, 403). If so, it'll try the authentication flow
* again. If it further encounters an unsuccessful response, then it'll throw a {@link
* RestClientException}
*
* @param request
Expand Down Expand Up @@ -261,7 +253,7 @@ protected void injectCsrfTokenIntoHeader(HttpRequest request, CsrfToken csrfToke
}

logger.debug(
"Injecting CSRF token into request {} header: {}", request.getURI(), csrfToken.getToken());
"Injecting CSRF token into request {} token: {}", request.getURI(), csrfToken.getToken());
request.getHeaders().add(csrfToken.getHeaderName(), csrfToken.getToken());
}

Expand All @@ -286,7 +278,7 @@ protected synchronized void startAuthenticationFlow(boolean usesPreAuthAuthentic
restTemplateForAuthenticationFlow.getForEntity(
restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()), String.class);

logger.error("login Resonse status code {}", loginResponseEntity.getStatusCode());
logger.debug("login Resonse status code {}", loginResponseEntity.getStatusCode());
if (!loginResponseEntity.hasBody()) {
throw new SessionAuthenticationException(
"Authentication failed: no CSRF token could be found. GET login status code = "
Expand All @@ -301,6 +293,7 @@ protected synchronized void startAuthenticationFlow(boolean usesPreAuthAuthentic
MultiValueMap<String, Object> loginPostParams = new LinkedMultiValueMap<>();
loginPostParams.add("username", credentialProvider.getUsername());
loginPostParams.add("password", credentialProvider.getPassword());
loginPostParams.add("_csrf", latestCsrfToken.getToken());

logger.debug(
"Post to login url to startAuthenticationFlow with user={}, pwd={}",
Expand All @@ -313,7 +306,7 @@ protected synchronized void startAuthenticationFlow(boolean usesPreAuthAuthentic
String.class);

// TODO(P1) This current way of checking if authentication is successful is somewhat
// hacky. Bascailly it says that authentication is successful if a 302 is returned
// hacky. Basically it says that authentication is successful if a 302 is returned
// and the redirect (from location header) maps to the login redirect path from the config.
URI locationURI = URI.create(postLoginResponseEntity.getHeaders().get("Location").get(0));
String expectedLocation =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.box.l10n.mojito.rest.resttemplate;

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
Expand All @@ -8,6 +11,7 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
Expand All @@ -20,8 +24,6 @@ public class ProxyCheckService implements InitializingBean {

@Autowired ResttemplateConfig restTemplateConfig;

@Autowired RestTemplate restTemplate;

@Override
public void afterPropertiesSet() {
hasProxy = isProxyConfigured();
Expand All @@ -31,11 +33,11 @@ public boolean hasProxy() {
return hasProxy;
}

protected boolean isProxyConfigured() {
private boolean isProxyConfigured() {

String testUrl =
UriComponentsBuilder.newInstance()
.scheme("http")
.scheme(restTemplateConfig.getProxyScheme())
.host(restTemplateConfig.getProxyHost())
.port(restTemplateConfig.getProxyPort())
.path("login")
Expand All @@ -44,15 +46,29 @@ protected boolean isProxyConfigured() {
logger.debug("Checking if proxy is configured with URL '{}'", testUrl);
HttpHeaders headers = new HttpHeaders();
headers.set("Host", restTemplateConfig.getHost());
logger.debug("With headers {}", headers);
HttpEntity<String> httpEntity = new HttpEntity<>(null, headers);
try {
RestTemplate tempRestTemplate = buildRestTemplate();
ResponseEntity<Void> response =
restTemplate.exchange(testUrl, HttpMethod.GET, httpEntity, Void.class);
tempRestTemplate.exchange(testUrl, HttpMethod.GET, httpEntity, Void.class);
logger.debug("Proxy login request response code {}", response.getStatusCode());
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
logger.warn("Unable to reach service via proxy. Defaulting to direct access", e.getMessage());
logger.warn(
"Proxy does not allow access to specified host. Falling back to directly accessing it",
e);
return false;
}
}

/***
* This is needed because the default RestTemplate does not allow the caller
* to set the Host header
*/
private RestTemplate buildRestTemplate() {
CloseableHttpClient httpClient =
HttpClients.custom().setDefaultRequestConfig(RequestConfig.custom().build()).build();
return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
}
}
Loading

0 comments on commit 70730b2

Please sign in to comment.