diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/AuthenticatedRestTemplate.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/AuthenticatedRestTemplate.java index b042a8981c..31bcd33693 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/AuthenticatedRestTemplate.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/AuthenticatedRestTemplate.java @@ -12,6 +12,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -49,17 +51,19 @@ public class AuthenticatedRestTemplate { /** logger */ static Logger logger = LoggerFactory.getLogger(AuthenticatedRestTemplate.class); - @Autowired ResttemplateConfig resttemplateConfig; + @Autowired ResttemplateConfig restTemplateConfig; /** Will delegate calls to the {@link RestTemplate} instance that was configured */ @Autowired CookieStoreRestTemplate restTemplate; /** Used to intercept requests and inject CSRF token */ - @Autowired - FormLoginAuthenticationCsrfTokenInterceptor formLoginAuthenticationCsrfTokenInterceptor; + @Autowired LoginAuthenticationCsrfTokenInterceptor loginAuthenticationCsrfTokenInterceptor; @Autowired RestTemplateUtil restTemplateUtil; + @Autowired(required = false) + ProxyOutboundRequestInterceptor proxyOutboundRequestInterceptor; + /** Initialize the internal restTemplate instance */ @PostConstruct protected void init() { @@ -70,8 +74,10 @@ protected void init() { logger.debug("Set interceptor for authentication"); List interceptors = - Collections.singletonList( - formLoginAuthenticationCsrfTokenInterceptor); + Collections.unmodifiableList( + Stream.of(proxyOutboundRequestInterceptor, loginAuthenticationCsrfTokenInterceptor) + .filter(Objects::nonNull) + .toList()); restTemplate.setRequestFactory( new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors)); @@ -121,7 +127,7 @@ protected void makeRestTemplateWithCustomObjectMapper(RestTemplate restTemplate) ObjectMapper objectMapper = jackson2ObjectMapperFactoryBean.getObject(); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - // To keep backward compatibility with the Joda output, disable write/reading nano seconds + // To keep backward compatibility with the Joda output, disable write/reading nanoseconds // with // Java time and ZonedDateTime // also see {@link com.box.l10n.mojito.json.ObjectMapper} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/CookieStoreRestTemplate.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/CookieStoreRestTemplate.java index 8431a267a0..307bbe58db 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/CookieStoreRestTemplate.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/CookieStoreRestTemplate.java @@ -31,7 +31,7 @@ public void setCookieStoreAndUpdateRequestFactory(CookieStore cookieStore) { HttpClientBuilder.create() .setDefaultCookieStore(cookieStore) // we have to turn off auto redirect in the rest template because - // when session expires, it will return a 302 and resttemplate + // when session expires, it will return a 302 and restTemplate // will automatically redirect to /login even before returning // the ClientHttpResponse in the interceptor .disableRedirectHandling() diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/FormLoginAuthenticationCsrfTokenInterceptor.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/LoginAuthenticationCsrfTokenInterceptor.java similarity index 68% rename from restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/FormLoginAuthenticationCsrfTokenInterceptor.java rename to restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/LoginAuthenticationCsrfTokenInterceptor.java index cc70321514..c2fc5209d9 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/FormLoginAuthenticationCsrfTokenInterceptor.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/LoginAuthenticationCsrfTokenInterceptor.java @@ -4,10 +4,11 @@ import jakarta.annotation.PostConstruct; import java.io.IOException; import java.net.URI; -import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.cookie.CookieStore; import org.slf4j.Logger; @@ -36,10 +37,10 @@ * @author wyau */ @Component -public class FormLoginAuthenticationCsrfTokenInterceptor implements ClientHttpRequestInterceptor { +public class LoginAuthenticationCsrfTokenInterceptor implements ClientHttpRequestInterceptor { /** logger */ - Logger logger = LoggerFactory.getLogger(FormLoginAuthenticationCsrfTokenInterceptor.class); + Logger logger = LoggerFactory.getLogger(LoginAuthenticationCsrfTokenInterceptor.class); public static final String CSRF_PARAM_NAME = "_csrf"; public static final String CSRF_HEADER_NAME = "X-CSRF-TOKEN"; @@ -77,6 +78,9 @@ public class FormLoginAuthenticationCsrfTokenInterceptor implements ClientHttpRe /** Will delegate calls to the {@link RestTemplate} instance that was configured */ @Autowired CookieStoreRestTemplate restTemplate; + @Autowired(required = false) + ProxyOutboundRequestInterceptor proxyOutboundRequestInterceptor; + /** Init */ @PostConstruct protected void init() { @@ -89,19 +93,22 @@ protected void init() { restTemplate.setCookieStoreAndUpdateRequestFactory(cookieStore); List interceptors = - Collections.singletonList( - new ClientHttpRequestInterceptor() { - @Override - public ClientHttpResponse intercept( - HttpRequest request, byte[] body, ClientHttpRequestExecution execution) - throws IOException { - if (latestCsrfToken != null) { - // At the beginning of auth flow, there's no token yet - injectCsrfTokenIntoHeader(request, latestCsrfToken); - } - return execution.execute(request, body); - } - }); + Stream.of( + proxyOutboundRequestInterceptor, + new ClientHttpRequestInterceptor() { + @Override + public ClientHttpResponse intercept( + HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + if (latestCsrfToken != null) { + // At the beginning of auth flow, there's no token yet + injectCsrfTokenIntoHeader(request, latestCsrfToken); + } + return execution.execute(request, body); + } + }) + .filter(Objects::nonNull) + .toList(); restTemplateForAuthenticationFlow.setRequestFactory( new InterceptingClientHttpRequestFactory( @@ -140,8 +147,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 @@ -202,7 +209,8 @@ private void reauthenticate(HttpRequest request) { protected void startAuthenticationAndInjectCsrfToken(HttpRequest request) { logger.debug( "Authenticate because no session is found in cookie store or it doesn't match with the one used to get the CSRF token we have."); - startAuthenticationFlow(); + // TODO: Remove logic once PreAuth logic is configured on all environments + startAuthenticationFlow(resttemplateConfig.usesLoginAuthentication()); logger.debug("Injecting CSRF token"); injectCsrfTokenIntoHeader(request, latestCsrfToken); @@ -248,72 +256,87 @@ 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()); } /** - * Starts the traditioanl form login authentication flow handshake. Consequencially, the cookie - * store (which contains the session id) and the CSRF token will be updated. + * If preAuthentication is enabled, then we merely get the CSRF token If preAuthentication is not + * enabled, start the traditional form login authentication flow handshake. + * + *

Consequentially, the cookie store (which contains the session id) and the CSRF token will be + * updated. * * @throws AuthenticationException */ - protected synchronized void startAuthenticationFlow() throws AuthenticationException { + protected synchronized void startAuthenticationFlow(boolean usesLoginAuthentication) + throws AuthenticationException { logger.debug("Getting authenticated session"); - logger.debug( - "Start by loading up the login form to get a valid unauthenticated session and CSRF token"); - ResponseEntity loginResponseEntity = - restTemplateForAuthenticationFlow.getForEntity( - restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()), String.class); - - latestCsrfToken = getCsrfTokenFromLoginHtml(loginResponseEntity.getBody()); - latestSessionIdForLatestCsrfToken = getAuthenticationSessionIdFromCookieStore(); - logger.debug( - "Update CSRF token for interceptor ({}) from login form", latestCsrfToken.getToken()); - - MultiValueMap loginPostParams = new LinkedMultiValueMap<>(); - loginPostParams.add("username", credentialProvider.getUsername()); - loginPostParams.add("password", credentialProvider.getPassword()); + if (usesLoginAuthentication) { + logger.debug( + "Start by loading up the login form to get a valid unauthenticated session and CSRF token"); + ResponseEntity loginResponseEntity = + restTemplateForAuthenticationFlow.getForEntity( + restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()), String.class); + + 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 = " + + loginResponseEntity.getStatusCode()); + } - logger.debug( - "Post to login url to startAuthenticationFlow with user={}, pwd={}", - credentialProvider.getUsername(), - credentialProvider.getPassword()); - ResponseEntity postLoginResponseEntity = - restTemplateForAuthenticationFlow.postForEntity( - restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()), - loginPostParams, - 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 - // 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 = - resttemplateConfig.getContextPath() + "/" + formLoginConfig.getLoginRedirectPath(); - - if (postLoginResponseEntity.getStatusCode().equals(HttpStatus.FOUND) - && expectedLocation.equals(locationURI.getPath())) { - - latestCsrfToken = - getCsrfTokenFromEndpoint( - restTemplateUtil.getURIForResource(formLoginConfig.getCsrfTokenPath())); + latestCsrfToken = getCsrfTokenFromLoginHtml(loginResponseEntity.getBody()); latestSessionIdForLatestCsrfToken = getAuthenticationSessionIdFromCookieStore(); - logger.debug( - "Update CSRF token interceptor in AuthRestTempplate ({})", latestCsrfToken.getToken()); + "Update CSRF token for interceptor ({}) from login form", latestCsrfToken.getToken()); - } else { - throw new SessionAuthenticationException( - "Authentication failed. Post login status code = " - + postLoginResponseEntity.getStatusCode() - + ", location = [" - + locationURI.getPath() - + "], expected location = [" - + expectedLocation - + "]"); + MultiValueMap 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={}", + credentialProvider.getUsername(), + credentialProvider.getPassword()); + ResponseEntity postLoginResponseEntity = + restTemplateForAuthenticationFlow.postForEntity( + restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()), + loginPostParams, + String.class); + + // TODO(P1) This current way of checking if authentication is successful is somewhat + // 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 = + resttemplateConfig.getContextPath() + "/" + formLoginConfig.getLoginRedirectPath(); + + boolean isAuthenticated = + postLoginResponseEntity.getStatusCode().equals(HttpStatus.FOUND) + && expectedLocation.equals(locationURI.getPath()); + + if (!isAuthenticated) { + throw new SessionAuthenticationException( + "Authentication failed. Post login status code = " + + postLoginResponseEntity.getStatusCode() + + ", location = [" + + locationURI.getPath() + + "], expected location = [" + + expectedLocation + + "]"); + } } + + latestCsrfToken = + getCsrfTokenFromEndpoint( + restTemplateUtil.getURIForResource(formLoginConfig.getCsrfTokenPath())); + latestSessionIdForLatestCsrfToken = getAuthenticationSessionIdFromCookieStore(); + + logger.debug( + "Update CSRF token interceptor in AuthRestTemplate ({})", latestCsrfToken.getToken()); } /** @@ -322,7 +345,7 @@ protected synchronized void startAuthenticationFlow() throws AuthenticationExcep * * @param loginHtml The login page HTML which contains the csrf token. It is assumed that the CSRF * token is embedded on the page inside an input field with name matching {@link - * com.box.l10n.mojito.rest.resttemplate.FormLoginAuthenticationCsrfTokenInterceptor#CSRF_PARAM_NAME} + * LoginAuthenticationCsrfTokenInterceptor#CSRF_PARAM_NAME} * @return * @throws AuthenticationException */ @@ -346,10 +369,20 @@ protected CsrfToken getCsrfTokenFromLoginHtml(String loginHtml) throws Authentic * @param csrfTokenUrl The full URL to which the CSRF token can be obtained * @return */ - protected CsrfToken getCsrfTokenFromEndpoint(String csrfTokenUrl) { + protected CsrfToken getCsrfTokenFromEndpoint(String csrfTokenUrl) + throws SessionAuthenticationException { ResponseEntity csrfTokenEntity = restTemplateForAuthenticationFlow.getForEntity(csrfTokenUrl, String.class, ""); logger.debug("CSRF token from {} is {}", csrfTokenUrl, csrfTokenEntity.getBody()); + if (csrfTokenEntity.getStatusCode().isError()) { + throw new SessionAuthenticationException( + "Authentication failed. GET login status code = " + + csrfTokenEntity.getStatusCode() + + ", location = [" + + csrfTokenUrl + + "]"); + } + return new DefaultCsrfToken(CSRF_HEADER_NAME, CSRF_PARAM_NAME, csrfTokenEntity.getBody()); } diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyConfig.java new file mode 100644 index 0000000000..76d8af6098 --- /dev/null +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyConfig.java @@ -0,0 +1,50 @@ +package com.box.l10n.mojito.rest.resttemplate; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ProxyConfig { + + @Value("${l10n.proxy.host:}") + String host; + + @Value("${l10n.proxy.scheme:http}") + String scheme; + + @Value("${l10n.proxy.port:19193}") + Integer port; + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getScheme() { + return scheme; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public boolean isValidConfiguration() { + return this.host != null + && this.scheme != null + && this.port != null + && !this.host.isEmpty() + && !this.scheme.isEmpty() + && this.port > 0; + } +} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthCheckService.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthCheckService.java new file mode 100644 index 0000000000..34248ac208 --- /dev/null +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthCheckService.java @@ -0,0 +1,43 @@ +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.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@ConditionalOnProperty("l10n.proxy.enabled") +public class ProxyHealthCheckService implements InitializingBean { + + private boolean isHealthy; + + @Autowired ResttemplateConfig restTemplateConfig; + + @Autowired ProxyConfig proxyConfig; + + @Override + public void afterPropertiesSet() { + RestTemplate restTemplate = buildRestTemplate(); + ProxyHealthChecker proxyHealthChecker = new ProxyHealthChecker(); + isHealthy = proxyHealthChecker.isProxyHealthy(restTemplate, restTemplateConfig, proxyConfig); + } + + public boolean isProxyHealthy() { + return isHealthy; + } + + /*** + * 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)); + } +} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthChecker.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthChecker.java new file mode 100644 index 0000000000..a582425b54 --- /dev/null +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthChecker.java @@ -0,0 +1,54 @@ +package com.box.l10n.mojito.rest.resttemplate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +public class ProxyHealthChecker { + Logger logger = LoggerFactory.getLogger(ProxyHealthChecker.class); + + public boolean isProxyHealthy( + RestTemplate restTemplate, ResttemplateConfig restTemplateConfig, ProxyConfig proxyConfig) { + + if (proxyConfig == null) { + logger.warn("Proxy configuration was not provided. Falling back to directly accessing host"); + return false; + } + + if (!proxyConfig.isValidConfiguration()) { + logger.warn( + "Proxy configuration is missing some required fields. Falling back to directly accessing host"); + return false; + } + + String testUrl = + UriComponentsBuilder.newInstance() + .scheme(proxyConfig.getScheme()) + .host(proxyConfig.getHost()) + .port(proxyConfig.getPort()) + .path("login") + .build() + .toUriString(); + 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 httpEntity = new HttpEntity<>(null, headers); + try { + ResponseEntity response = + restTemplate.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( + "Proxy does not allow access to specified host. Falling back to directly accessing it", + e); + return false; + } + } +} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyOutboundRequestInterceptor.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyOutboundRequestInterceptor.java new file mode 100644 index 0000000000..986542f13e --- /dev/null +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ProxyOutboundRequestInterceptor.java @@ -0,0 +1,93 @@ +package com.box.l10n.mojito.rest.resttemplate; + +import java.io.IOException; +import java.net.URI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +@ConditionalOnProperty("l10n.proxy.enabled") +public class ProxyOutboundRequestInterceptor implements ClientHttpRequestInterceptor { + + Logger logger = LoggerFactory.getLogger(ProxyOutboundRequestInterceptor.class); + + @Autowired ResttemplateConfig restTemplateConfig; + + @Autowired(required = false) + ProxyHealthCheckService proxyHealthCheckService; + + @Autowired(required = false) + ProxyConfig proxyConfig; + + @Override + public ClientHttpResponse intercept( + HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + if (proxyConfig == null || !proxyConfig.isValidConfiguration()) { + return execution.execute(request, body); + } + + if (proxyHealthCheckService.isProxyHealthy()) { + // To prevent adding extra layers, when the proxied request fails and is retried + if (request.getURI().getHost().equals(proxyConfig.getHost())) { + logger.debug("Proxy has already been configured for request"); + return execution.execute(request, body); + } + + logger.debug("Configuring request via proxy"); + String rawPath = request.getURI().getRawPath(); + String rawQuery = request.getURI().getRawQuery(); + URI proxyUri = + UriComponentsBuilder.newInstance() + .scheme(proxyConfig.getScheme()) + .host(proxyConfig.getHost()) + .port(proxyConfig.getPort()) + .path(rawPath) + .query(rawQuery) + .build() + .toUri(); + + HttpRequest modifiedRequest = buildProxiedRequest(request, proxyUri); + logger.debug( + "Modified Proxy Request. Method: {}. Uri: {}. Headers: {}", + modifiedRequest.getMethod(), + modifiedRequest.getURI(), + modifiedRequest.getHeaders()); + return execution.execute(modifiedRequest, body); + } + + logger.debug("Proxy is not configured for request"); + return execution.execute(request, body); + } + + private HttpRequest buildProxiedRequest(HttpRequest request, URI proxyUri) { + return new HttpRequest() { + @Override + public HttpMethod getMethod() { + return request.getMethod(); + } + + @Override + public URI getURI() { + return proxyUri; + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.putAll(request.getHeaders()); + headers.set("Host", restTemplateConfig.getHost()); + return headers; + } + }; + } +} diff --git a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ResttemplateConfig.java b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ResttemplateConfig.java index 965c848e0e..c72adf2ae6 100644 --- a/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ResttemplateConfig.java +++ b/restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ResttemplateConfig.java @@ -16,6 +16,7 @@ public class ResttemplateConfig { Integer port = 8080; String scheme = "http"; String contextPath = ""; + boolean usesLoginAuthentication = true; Authentication authentication = new Authentication(); @@ -95,4 +96,12 @@ public String getContextPath() { public void setContextPath(String contextPath) { this.contextPath = contextPath; } + + public boolean usesLoginAuthentication() { + return usesLoginAuthentication; + } + + public void setUsesLoginAuthentication(boolean usesLoginAuthentication) { + this.usesLoginAuthentication = usesLoginAuthentication; + } } diff --git a/restclient/src/test/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthCheckerTest.java b/restclient/src/test/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthCheckerTest.java new file mode 100644 index 0000000000..ecd2129cb7 --- /dev/null +++ b/restclient/src/test/java/com/box/l10n/mojito/rest/resttemplate/ProxyHealthCheckerTest.java @@ -0,0 +1,89 @@ +package com.box.l10n.mojito.rest.resttemplate; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +/** + * @author byronantak + */ +class ProxyHealthCheckerTest { + + @Mock private RestTemplate restTemplate; + + ProxyHealthChecker proxyHealthChecker; + + ProxyConfig proxyConfig; + + @BeforeEach + void init() { + MockitoAnnotations.openMocks(this); + proxyHealthChecker = new ProxyHealthChecker(); + proxyConfig = new ProxyConfig(); + proxyConfig.setHost("localhost"); + proxyConfig.setPort(19193); + proxyConfig.setScheme("http"); + } + + @Test + void shouldReturnFalseWhenProxyConfigIsNullOrIncomplete() { + when(restTemplate.exchange( + anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(Void.class))) + .thenReturn(new ResponseEntity(HttpStatus.OK)); + + boolean isHealthy = + proxyHealthChecker.isProxyHealthy(restTemplate, new ResttemplateConfig(), null); + assertFalse(isHealthy); + isHealthy = + proxyHealthChecker.isProxyHealthy( + restTemplate, new ResttemplateConfig(), new ProxyConfig()); + assertFalse(isHealthy); + } + + @Test + void shouldReturnTrueWhenProxyRequestRespondsWithA200() { + when(restTemplate.exchange( + anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(Void.class))) + .thenReturn(new ResponseEntity(HttpStatus.OK)); + + boolean isHealthy = + proxyHealthChecker.isProxyHealthy(restTemplate, new ResttemplateConfig(), proxyConfig); + assertTrue(isHealthy); + } + + @Test + void shouldReturnFalseWhenProxyRequestRaisesException() { + when(restTemplate.exchange( + anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(Void.class))) + .thenThrow(new RestClientException("An error occurred")); + + boolean isHealthy = + proxyHealthChecker.isProxyHealthy(restTemplate, new ResttemplateConfig(), proxyConfig); + assertFalse(isHealthy); + } + + @Test + void shouldReturnFalseWhenProxyRequestReturnsBadStatusCode() { + when(restTemplate.exchange( + anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(Void.class))) + .thenReturn(new ResponseEntity(HttpStatus.NOT_FOUND)); + + boolean isHealthy = + proxyHealthChecker.isProxyHealthy(restTemplate, new ResttemplateConfig(), proxyConfig); + assertFalse(isHealthy); + } +} diff --git a/webapp/src/test/java/com/box/l10n/mojito/security/AuthenticationTest.java b/webapp/src/test/java/com/box/l10n/mojito/security/AuthenticationTest.java index b18f27ff77..8cd443c26e 100644 --- a/webapp/src/test/java/com/box/l10n/mojito/security/AuthenticationTest.java +++ b/webapp/src/test/java/com/box/l10n/mojito/security/AuthenticationTest.java @@ -2,7 +2,7 @@ import com.box.l10n.mojito.rest.WSTestBase; import com.box.l10n.mojito.rest.resttemplate.CredentialProvider; -import com.box.l10n.mojito.rest.resttemplate.FormLoginAuthenticationCsrfTokenInterceptor; +import com.box.l10n.mojito.rest.resttemplate.LoginAuthenticationCsrfTokenInterceptor; import java.io.IOException; import org.apache.hc.client5.http.cookie.CookieStore; import org.apache.http.HttpStatus; @@ -23,8 +23,7 @@ public class AuthenticationTest extends WSTestBase { /** logger */ static Logger logger = LoggerFactory.getLogger(AuthenticationTest.class); - @Autowired - FormLoginAuthenticationCsrfTokenInterceptor formLoginAuthenticationCsrfTokenInterceptor; + @Autowired LoginAuthenticationCsrfTokenInterceptor loginAuthenticationCsrfTokenInterceptor; @Autowired CredentialProvider credentialProvider; @@ -32,10 +31,10 @@ public class AuthenticationTest extends WSTestBase { public void before() { logger.debug( "Resetting authenticated session in rest template so the next time will have to re-auth"); - formLoginAuthenticationCsrfTokenInterceptor.resetAuthentication(); + loginAuthenticationCsrfTokenInterceptor.resetAuthentication(); logger.debug("Resetting credential as it might have been polutted in another test"); - formLoginAuthenticationCsrfTokenInterceptor.setCredentialProvider(credentialProvider); + loginAuthenticationCsrfTokenInterceptor.setCredentialProvider(credentialProvider); } @Test @@ -70,7 +69,7 @@ public void testSuccessfulAuthAfterResettingCookieStore() { @Test(expected = SessionAuthenticationException.class) public void testUnsuccessfulAuthWithIncorrectUser() throws IOException { - formLoginAuthenticationCsrfTokenInterceptor.setCredentialProvider( + loginAuthenticationCsrfTokenInterceptor.setCredentialProvider( new CredentialProvider() { @Override public String getUsername() { @@ -88,7 +87,7 @@ public String getPassword() { @Test(expected = SessionAuthenticationException.class) public void testUnsuccessfulAuthWithIncorrectPassword() throws IOException { - formLoginAuthenticationCsrfTokenInterceptor.setCredentialProvider( + loginAuthenticationCsrfTokenInterceptor.setCredentialProvider( new CredentialProvider() { @Override public String getUsername() {