Skip to content

Commit 0580d89

Browse files
authored
[EWT-191] Add support for custom proxy configurations with authentication (#202)
1 parent f84b438 commit 0580d89

File tree

7 files changed

+169
-17
lines changed

7 files changed

+169
-17
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Main properties
22
group=com.truelayer
33
archivesBaseName=truelayer-java
4-
version=6.2.0
4+
version=6.3.0
55

66
# Artifacts properties
77
sonatype_repository_url=https://s01.oss.sonatype.org/service/local/

src/main/java/com/truelayer/java/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static final class HeaderNames {
2828
public static final String TL_SIGNATURE = "Tl-Signature";
2929
public static final String TL_AGENT = "TL-Agent";
3030
public static final String AUTHORIZATION = "Authorization";
31+
public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
3132
public static final String X_FORWARDED_FOR = "X-Forwarded-For";
3233
public static final String COOKIE = "Cookie";
3334
public static final String TL_CORRELATION_ID = "X-Tl-Correlation-Id";
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.truelayer.java;
2+
3+
import lombok.Builder;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.Getter;
6+
import lombok.ToString;
7+
import lombok.experimental.Accessors;
8+
9+
/**
10+
* Class that represent a custom proxy configuration
11+
*/
12+
@Builder
13+
@Getter
14+
@EqualsAndHashCode
15+
@ToString
16+
@Accessors(fluent = true)
17+
public class ProxyConfiguration {
18+
19+
/**
20+
* Hostname of the proxy
21+
*/
22+
String hostname;
23+
24+
/**
25+
* Port of the proxy
26+
*/
27+
int port;
28+
29+
/**
30+
* Optional credentials for authenticating proxy requests
31+
*/
32+
Credentials credentials;
33+
34+
@Builder
35+
@Getter
36+
@EqualsAndHashCode
37+
@ToString
38+
@Accessors(fluent = true)
39+
public static class Credentials {
40+
String username;
41+
42+
String password;
43+
}
44+
}

src/main/java/com/truelayer/java/TrueLayerClientBuilder.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public class TrueLayerClientBuilder {
6060

6161
private ICredentialsCache credentialsCache;
6262

63+
private ProxyConfiguration proxyConfiguration;
64+
6365
TrueLayerClientBuilder() {}
6466

6567
/**
@@ -167,6 +169,16 @@ public TrueLayerClientBuilder withCredentialsCaching(ICredentialsCache credentia
167169
return this;
168170
}
169171

172+
/**
173+
* Utility to configure a custom proxy, optionally including an authentication.
174+
* @param proxyConfiguration the configuration describing the custom proxy
175+
* @return the instance of the client builder used
176+
*/
177+
public TrueLayerClientBuilder withProxyConfiguration(ProxyConfiguration proxyConfiguration) {
178+
this.proxyConfiguration = proxyConfiguration;
179+
return this;
180+
}
181+
170182
/**
171183
* Builds the Java library main class to interact with TrueLayer APIs.
172184
* @return a client instance
@@ -180,7 +192,7 @@ public TrueLayerClient build() {
180192
OkHttpClientFactory httpClientFactory = new OkHttpClientFactory(new LibraryInfoLoader());
181193

182194
OkHttpClient baseHttpClient = httpClientFactory.buildBaseApiClient(
183-
timeout, connectionPoolOptions, requestExecutor, logMessageConsumer);
195+
timeout, connectionPoolOptions, requestExecutor, logMessageConsumer, proxyConfiguration);
184196

185197
OkHttpClient authHttpClient = httpClientFactory.buildAuthApiClient(baseHttpClient, clientCredentials);
186198

src/main/java/com/truelayer/java/http/OkHttpClientFactory.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
import static org.apache.commons.lang3.ObjectUtils.isEmpty;
44
import static org.apache.commons.lang3.ObjectUtils.isNotEmpty;
55

6-
import com.truelayer.java.ClientCredentials;
7-
import com.truelayer.java.ConnectionPoolOptions;
6+
import com.truelayer.java.*;
87
import com.truelayer.java.ConnectionPoolOptions.KeepAliveDuration;
9-
import com.truelayer.java.SigningOptions;
10-
import com.truelayer.java.TrueLayerException;
118
import com.truelayer.java.auth.IAuthenticationHandler;
129
import com.truelayer.java.http.auth.AccessTokenInvalidator;
1310
import com.truelayer.java.http.auth.AccessTokenManager;
@@ -19,13 +16,13 @@
1916
import com.truelayer.java.http.interceptors.logging.HttpLoggingInterceptor;
2017
import com.truelayer.java.http.interceptors.logging.SensitiveHeaderGuard;
2118
import com.truelayer.java.versioninfo.LibraryInfoLoader;
19+
import java.net.InetSocketAddress;
20+
import java.net.Proxy;
2221
import java.time.Duration;
2322
import java.util.concurrent.ExecutorService;
2423
import java.util.function.Consumer;
2524
import lombok.Value;
26-
import okhttp3.ConnectionPool;
27-
import okhttp3.Dispatcher;
28-
import okhttp3.OkHttpClient;
25+
import okhttp3.*;
2926

3027
@Value
3128
public class OkHttpClientFactory {
@@ -41,7 +38,8 @@ public OkHttpClient buildBaseApiClient(
4138
Duration timeout,
4239
ConnectionPoolOptions connectionPoolOptions,
4340
ExecutorService requestExecutor,
44-
Consumer<String> logMessageConsumer) {
41+
Consumer<String> logMessageConsumer,
42+
ProxyConfiguration proxyConfiguration) {
4543

4644
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
4745

@@ -76,6 +74,24 @@ public OkHttpClient buildBaseApiClient(
7674
new HttpLoggingInterceptor(logMessageConsumer, new SensitiveHeaderGuard()));
7775
}
7876

77+
if (isNotEmpty(proxyConfiguration)) {
78+
clientBuilder.proxy(new Proxy(
79+
Proxy.Type.HTTP, new InetSocketAddress(proxyConfiguration.hostname(), proxyConfiguration.port())));
80+
81+
if (isNotEmpty(proxyConfiguration.credentials())) {
82+
clientBuilder.proxyAuthenticator((route, response) -> {
83+
String credentials = Credentials.basic(
84+
proxyConfiguration.credentials().username(),
85+
proxyConfiguration.credentials().password());
86+
87+
return response.request()
88+
.newBuilder()
89+
.header("Proxy-Authorization", credentials)
90+
.build();
91+
});
92+
}
93+
}
94+
7995
clientBuilder.addInterceptor(new TrueLayerAgentInterceptor(libraryInfoLoader.load()));
8096

8197
return clientBuilder.build();

src/test/java/com/truelayer/java/TrueLayerClientBuilderTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.truelayer.java;
22

3-
import static com.truelayer.java.TestUtils.getClientCredentials;
4-
import static com.truelayer.java.TestUtils.getSigningOptions;
3+
import static com.truelayer.java.TestUtils.*;
54
import static org.junit.jupiter.api.Assertions.*;
65
import static org.mockito.Mockito.mock;
76

src/test/java/com/truelayer/java/http/OkHttpClientFactoryTests.java

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package com.truelayer.java.http;
22

3+
import static com.truelayer.java.Constants.HeaderNames.PROXY_AUTHORIZATION;
34
import static com.truelayer.java.TestUtils.getClientCredentials;
45
import static org.junit.jupiter.api.Assertions.*;
56
import static org.mockito.Mockito.mock;
67
import static org.mockito.Mockito.when;
78

89
import com.truelayer.java.ConnectionPoolOptions;
10+
import com.truelayer.java.ProxyConfiguration;
911
import com.truelayer.java.TestUtils;
1012
import com.truelayer.java.auth.AuthenticationHandler;
1113
import com.truelayer.java.auth.IAuthenticationHandler;
@@ -17,13 +19,15 @@
1719
import com.truelayer.java.http.interceptors.logging.HttpLoggingInterceptor;
1820
import com.truelayer.java.versioninfo.LibraryInfoLoader;
1921
import com.truelayer.java.versioninfo.VersionInfo;
22+
import java.net.Proxy;
2023
import java.net.URI;
2124
import java.time.Clock;
2225
import java.time.Duration;
2326
import java.util.concurrent.ExecutorService;
2427
import java.util.concurrent.Executors;
2528
import java.util.function.Consumer;
26-
import okhttp3.OkHttpClient;
29+
import lombok.SneakyThrows;
30+
import okhttp3.*;
2731
import org.junit.jupiter.api.DisplayName;
2832
import org.junit.jupiter.api.Test;
2933

@@ -39,7 +43,7 @@ public void shouldCreateABaseAuthApiClient() {
3943

4044
OkHttpClient baseApiClient = getOkHttpClientFactory()
4145
.buildBaseApiClient(
42-
null, ConnectionPoolOptions.builder().build(), customExecutor, customLogMessageConsumer);
46+
null, ConnectionPoolOptions.builder().build(), customExecutor, customLogMessageConsumer, null);
4347

4448
assertNotNull(baseApiClient);
4549
assertTrue(
@@ -78,7 +82,8 @@ public void shouldCreateABaseAuthApiClientWithCustomCallTimeout() {
7882
customTimeout,
7983
ConnectionPoolOptions.builder().build(),
8084
customExecutor,
81-
customLogMessageConsumer);
85+
customLogMessageConsumer,
86+
null);
8287

8388
assertNotNull(baseApiClient);
8489
assertEquals(0, baseApiClient.connectTimeoutMillis(), "Unexpected connect timeout configured");
@@ -87,6 +92,80 @@ public void shouldCreateABaseAuthApiClientWithCustomCallTimeout() {
8792
assertEquals(customTimeout.toMillis(), baseApiClient.callTimeoutMillis(), "Unexpected call timeout configured");
8893
}
8994

95+
@SneakyThrows
96+
@Test
97+
@DisplayName("It should build a Base API client with custom unauthenticated proxy configuration")
98+
public void shouldCreateABaseAuthApiClientWithCustomProxyConfigNoAuthentication() {
99+
ProxyConfiguration customProxyConfig =
100+
ProxyConfiguration.builder().hostname("127.0.0.1").port(9999).build();
101+
102+
OkHttpClient baseApiClient = getOkHttpClientFactory()
103+
.buildBaseApiClient(null, ConnectionPoolOptions.builder().build(), null, null, customProxyConfig);
104+
105+
assertNotNull(baseApiClient);
106+
Proxy configuredProxy = baseApiClient.proxy();
107+
assertNotNull(configuredProxy, "proxy not configured");
108+
assertEquals(Proxy.Type.HTTP, configuredProxy.type(), "unexpected proxy type configured");
109+
assertTrue(
110+
configuredProxy
111+
.address()
112+
.toString()
113+
.endsWith(customProxyConfig.hostname() + ":" + customProxyConfig.port()),
114+
"unexpected proxy address found");
115+
Authenticator proxyAuthenticator = baseApiClient.proxyAuthenticator();
116+
// make sure the authenticator is the default used by OkHttp.
117+
assertInstanceOf(Authenticator.NONE.getClass(), proxyAuthenticator);
118+
}
119+
120+
@SneakyThrows
121+
@Test
122+
@DisplayName("It should build a Base API client with custom proxy configuration with authentication")
123+
public void shouldCreateABaseAuthApiClientWithCustomProxyConfigWithAuthentication() {
124+
ProxyConfiguration customProxyConfig = ProxyConfiguration.builder()
125+
.hostname("127.0.0.1")
126+
.port(9999)
127+
.credentials(ProxyConfiguration.Credentials.builder()
128+
.username("john")
129+
.password("doe")
130+
.build())
131+
.build();
132+
133+
OkHttpClient baseApiClient = getOkHttpClientFactory()
134+
.buildBaseApiClient(null, ConnectionPoolOptions.builder().build(), null, null, customProxyConfig);
135+
136+
assertNotNull(baseApiClient);
137+
Proxy configuredProxy = baseApiClient.proxy();
138+
assertNotNull(configuredProxy, "proxy not configured");
139+
assertEquals(Proxy.Type.HTTP, configuredProxy.type(), "unexpected proxy type configured");
140+
assertTrue(
141+
configuredProxy
142+
.address()
143+
.toString()
144+
.endsWith(customProxyConfig.hostname() + ":" + customProxyConfig.port()),
145+
"unexpected proxy address found");
146+
Authenticator proxyAuthenticator = baseApiClient.proxyAuthenticator();
147+
assertNotNull(proxyAuthenticator, "proxy authenticator not configured");
148+
Request testRequest = proxyAuthenticator.authenticate(
149+
null,
150+
new Response.Builder()
151+
.protocol(Protocol.HTTP_2)
152+
.code(200)
153+
.message("A response")
154+
.request(new Request.Builder()
155+
.url("https://localhost/test")
156+
.get()
157+
.build())
158+
.build());
159+
assertNotNull(testRequest, "invalid request generated");
160+
String expectedProxyAuthHeader = Credentials.basic(
161+
customProxyConfig.credentials().username(),
162+
customProxyConfig.credentials().password());
163+
assertEquals(
164+
expectedProxyAuthHeader,
165+
testRequest.header(PROXY_AUTHORIZATION),
166+
"unexpected Proxy-Authentication header found");
167+
}
168+
90169
@Test
91170
@DisplayName("It should build an Auth API client")
92171
public void shouldCreateAnAuthApiClient() {
@@ -99,7 +178,8 @@ public void shouldCreateAnAuthApiClient() {
99178
customTimeout,
100179
ConnectionPoolOptions.builder().build(),
101180
customExecutor,
102-
customLogMessageConsumer);
181+
customLogMessageConsumer,
182+
null);
103183

104184
OkHttpClient authClient = getOkHttpClientFactory().buildAuthApiClient(baseApiClient, getClientCredentials());
105185

@@ -121,7 +201,7 @@ public void shouldCreateAnAuthApiClient() {
121201
@Test
122202
@DisplayName("It should build a Payments API client")
123203
public void shouldThrowCredentialsMissingException() {
124-
OkHttpClient baseHttpClient = getOkHttpClientFactory().buildBaseApiClient(null, null, null, null);
204+
OkHttpClient baseHttpClient = getOkHttpClientFactory().buildBaseApiClient(null, null, null, null, null);
125205
OkHttpClient authClient = getOkHttpClientFactory().buildAuthApiClient(baseHttpClient, getClientCredentials());
126206

127207
IAuthenticationHandler authenticationHandler = AuthenticationHandler.New()

0 commit comments

Comments
 (0)