Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit 07679ec

Browse files
ghillertjvalkeal
authored andcommitted
gh-1896 Shell: Add Proxy Support
* Add support for specifying an optional proxy server for the Shell - Allow to provide credentials (username/password) - Support SSL connectivity * Update documentation * Add basic tests
1 parent 2ca6059 commit 07679ec

File tree

9 files changed

+294
-10
lines changed

9 files changed

+294
-10
lines changed

spring-cloud-dataflow-docs/src/main/asciidoc/shell.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ The shell is built upon the link:https://projects.spring.io/spring-shell/[Spring
1414
There are command line options generic to Spring Shell and some specific to Data Flow.
1515
The shell takes the following command line options
1616

17-
[source,bash,options="nowrap"]
17+
[source,bash,options="nowrap",subs=attributes]
1818
----
19-
unix:>java -jar spring-cloud-dataflow-shell-1.2.1.RELEASE.jar --help
19+
unix:>java -jar spring-cloud-dataflow-shell-{project-version}.jar --help
2020
Data Flow Options:
2121
--dataflow.uri=<uri> Address of the Data Flow Server [default: http://localhost:9393].
2222
--dataflow.username=<USER> Username of the Data Flow Server [no default].
@@ -25,6 +25,9 @@ Data Flow Options:
2525
OAuth Bearer Token (Access Token prefixed with 'Bearer '),
2626
e.g. 'Bearer 12345'), [no default].
2727
--dataflow.skip-ssl-validation=<true|false> Accept any SSL certificate (even self-signed) [default: no].
28+
--dataflow.proxy.uri=<PROXY-URI> Address of an optional proxy server to use [no default].
29+
--dataflow.proxy.username=<PROXY-USERNAME> Username of the proxy server (if required by proxy server) [no default].
30+
--dataflow.proxy.password=<PROXY-PASSWORD> Password of the proxy server (if required by proxy server) [no default].
2831
--spring.shell.historySize=<SIZE> Default size of the shell log file [default: 3000].
2932
--spring.shell.commandFile=<FILE> Data Flow Shell executes commands read from the file(s) and then exits.
3033
--help This message.

spring-cloud-dataflow-rest-resource/src/main/java/org/springframework/cloud/dataflow/rest/util/HttpClientConfigurer.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import org.apache.http.HttpRequestInterceptor;
2222
import org.apache.http.auth.AuthScope;
2323
import org.apache.http.auth.UsernamePasswordCredentials;
24+
import org.apache.http.client.CredentialsProvider;
2425
import org.apache.http.conn.ssl.NoopHostnameVerifier;
2526
import org.apache.http.impl.client.BasicCredentialsProvider;
2627
import org.apache.http.impl.client.CloseableHttpClient;
2728
import org.apache.http.impl.client.HttpClientBuilder;
29+
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
2830

2931
import org.springframework.http.client.ClientHttpRequestFactory;
3032
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
33+
import org.springframework.util.Assert;
3134

3235
/**
3336
* Utility for configuring a {@link CloseableHttpClient}. This class allows for
@@ -66,6 +69,34 @@ public HttpClientConfigurer basicAuthCredentials(String username, String passwor
6669
return this;
6770
}
6871

72+
/**
73+
* Configures the {@link HttpClientBuilder} with a proxy host. If the
74+
* {@code proxyUsername} and {@code proxyPassword} are not {@code null}
75+
* then a {@link CredentialsProvider} is also configured for the proxy host.
76+
*
77+
* @param proxyUri Must not be null and must be configured with a scheme (http or https).
78+
* @param proxyUsername May be null
79+
* @param proxyPassword May be null
80+
* @return a reference to {@code this} to enable chained method invocation
81+
*/
82+
public HttpClientConfigurer withProxyCredentials(URI proxyUri, String proxyUsername, String proxyPassword) {
83+
84+
Assert.notNull(proxyUri, "The proxyUri must not be null.");
85+
Assert.hasText(proxyUri.getScheme(), "The scheme component of the proxyUri must not be empty.");
86+
87+
httpClientBuilder
88+
.setProxy(new HttpHost(proxyUri.getHost(), proxyUri.getPort(), proxyUri.getScheme()));
89+
if (proxyUsername !=null && proxyPassword != null) {
90+
final CredentialsProvider proxyCredsProvider = new BasicCredentialsProvider();
91+
proxyCredsProvider.setCredentials(
92+
new AuthScope(proxyUri.getHost(), proxyUri.getPort()),
93+
new UsernamePasswordCredentials(proxyUsername, proxyPassword));
94+
httpClientBuilder.setDefaultCredentialsProvider(proxyCredsProvider)
95+
.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
96+
}
97+
return this;
98+
}
99+
69100
/**
70101
* Sets the client's {@link javax.net.ssl.SSLContext} to use
71102
* {@link HttpUtils#buildCertificateIgnoringSslContext()}.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.cloud.dataflow.rest.util;
17+
18+
import java.net.URI;
19+
20+
import org.apache.http.client.HttpClient;
21+
import org.junit.Assert;
22+
import org.junit.Test;
23+
24+
import static org.junit.Assert.fail;
25+
26+
/**
27+
* @author Gunnar Hillert
28+
* @since 1.4
29+
*/
30+
public class HttpClientConfigurerTests {
31+
32+
/**
33+
* Basic test ensuring that the {@link HttpClient} is built successfully.
34+
*/
35+
@Test
36+
public void testThatHttpClientWithProxyIsCreated() {
37+
final HttpClientConfigurer builder = HttpClientConfigurer.create();
38+
builder.withProxyCredentials(URI.create("https://spring.io"), "spring", "cloud");
39+
builder.buildHttpClient();
40+
}
41+
42+
/**
43+
* Basic test ensuring that the {@link HttpClient} is built successfully with
44+
* null username and password.
45+
*/
46+
@Test
47+
public void testThatHttpClientWithProxyIsCreatedWithNullUsernameAndPassword() {
48+
final HttpClientConfigurer builder = HttpClientConfigurer.create();
49+
builder.withProxyCredentials(URI.create("https://spring.io"), null, null);
50+
builder.buildHttpClient();
51+
}
52+
53+
/**
54+
* Basic test ensuring that an exception is thrown if the scheme of the proxy
55+
* Uri is not set.
56+
*/
57+
@Test
58+
public void testHttpClientWithProxyCreationWithMissingScheme() {
59+
final HttpClientConfigurer builder = HttpClientConfigurer.create();
60+
try {
61+
builder.withProxyCredentials(URI.create("spring"), "spring", "cloud");
62+
}
63+
catch (IllegalArgumentException e) {
64+
Assert.assertEquals("The scheme component of the proxyUri must not be empty.", e.getMessage());
65+
return;
66+
}
67+
fail("Expected an IllegalArgumentException to be thrown.");
68+
}
69+
70+
/**
71+
* Basic test ensuring that an exception is thrown if the proxy
72+
* Uri is null.
73+
*/
74+
@Test
75+
public void testHttpClientWithNullProxyUri() {
76+
final HttpClientConfigurer builder = HttpClientConfigurer.create();
77+
try {
78+
builder.withProxyCredentials(null, null, null);
79+
}
80+
catch (IllegalArgumentException e) {
81+
Assert.assertEquals("The proxyUri must not be null.", e.getMessage());
82+
return;
83+
}
84+
fail("Expected an IllegalArgumentException to be thrown.");
85+
}
86+
}

spring-cloud-dataflow-shell-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,11 @@
8686
<artifactId>spring-boot-configuration-processor</artifactId>
8787
<optional>true</optional>
8888
</dependency>
89+
<dependency>
90+
<groupId>org.littleshoot</groupId>
91+
<artifactId>littleproxy</artifactId>
92+
<version>1.1.2</version>
93+
<scope>test</scope>
94+
</dependency>
8995
</dependencies>
9096
</project>

spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/Target.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2017 the original author or authors.
2+
* Copyright 2016-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,6 +50,14 @@ public class Target {
5050

5151
public static final String DEFAULT_TARGET = DEFAULT_SCHEME + "://" + DEFAULT_HOST + ":" + DEFAULT_PORT + "/";
5252

53+
public static final String DEFAULT_PROXY_USERNAME = "";
54+
55+
public static final String DEFAULT_PROXY_SPECIFIED_PASSWORD = "";
56+
57+
public static final String DEFAULT_PROXY_UNSPECIFIED_PASSWORD = "__NULL__";
58+
59+
public static final String DEFAULT_PROXY_URI = "";
60+
5361
private final URI targetUri;
5462

5563
private final boolean skipSslValidation;

spring-cloud-dataflow-shell-core/src/main/java/org/springframework/cloud/dataflow/shell/command/common/ConfigCommands.java

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.PrintWriter;
2020
import java.io.StringWriter;
21+
import java.net.URI;
2122
import java.util.ArrayList;
2223
import java.util.Arrays;
2324
import java.util.LinkedHashMap;
@@ -122,6 +123,15 @@ public class ConfigCommands implements CommandMarker, InitializingBean, Applicat
122123
@Value("${dataflow.skip-ssl-validation:" + Target.DEFAULT_UNSPECIFIED_SKIP_SSL_VALIDATION + "}")
123124
private boolean skipSslValidation;
124125

126+
@Value("${dataflow.proxy.uri:" + Target.DEFAULT_PROXY_URI + "}")
127+
private String proxyUri;
128+
129+
@Value("${dataflow.proxy.username:" + Target.DEFAULT_PROXY_USERNAME + "}")
130+
private String proxyUsername;
131+
132+
@Value("${dataflow.proxy.password:" + Target.DEFAULT_PROXY_SPECIFIED_PASSWORD + "}")
133+
private String proxyPassword;
134+
125135
@Value("${dataflow.credentials-provider-command:" + Target.DEFAULT_CREDENTIALS_PROVIDER_COMMAND + "}")
126136
private String credentialsProviderCommand;
127137

@@ -182,7 +192,16 @@ public String target(
182192
@CliOption(mandatory = false, key = {
183193
"credentials-provider-command" }, help = "a command to run that outputs the HTTP credentials used for authentication", unspecifiedDefaultValue = Target.DEFAULT_CREDENTIALS_PROVIDER_COMMAND) String credentialsProviderCommand,
184194
@CliOption(mandatory = false, key = {
185-
"skip-ssl-validation" }, help = "accept any SSL certificate (even self-signed)", specifiedDefaultValue = Target.DEFAULT_SPECIFIED_SKIP_SSL_VALIDATION, unspecifiedDefaultValue = Target.DEFAULT_UNSPECIFIED_SKIP_SSL_VALIDATION) boolean skipSslValidation) {
195+
"skip-ssl-validation" }, help = "accept any SSL certificate (even self-signed)", specifiedDefaultValue = Target.DEFAULT_SPECIFIED_SKIP_SSL_VALIDATION, unspecifiedDefaultValue = Target.DEFAULT_UNSPECIFIED_SKIP_SSL_VALIDATION) boolean skipSslValidation,
196+
197+
@CliOption(mandatory = false, key = {
198+
"proxy-uri" }, help = "the uri of the proxy server", specifiedDefaultValue = Target.DEFAULT_SPECIFIED_PASSWORD, unspecifiedDefaultValue = Target.DEFAULT_UNSPECIFIED_PASSWORD) String proxyUri,
199+
@CliOption(mandatory = false, key = {
200+
"proxy-username" }, help = "the username for authenticated access to the secured proxy server (valid only with a "
201+
+ "username)", specifiedDefaultValue = Target.DEFAULT_SPECIFIED_PASSWORD, unspecifiedDefaultValue = Target.DEFAULT_UNSPECIFIED_PASSWORD) String proxyUsername,
202+
@CliOption(mandatory = false, key = {
203+
"proxy-password" }, help = "the password for authenticated access to the secured proxy server (valid only with a "
204+
+ "username)", specifiedDefaultValue = Target.DEFAULT_SPECIFIED_PASSWORD, unspecifiedDefaultValue = Target.DEFAULT_UNSPECIFIED_PASSWORD) String proxyPassword) {
186205
if (StringUtils.isEmpty(credentialsProviderCommand) &&
187206
!StringUtils.isEmpty(targetPassword) && StringUtils.isEmpty(targetUsername)) {
188207
return "A password may be specified only together with a username";
@@ -208,6 +227,13 @@ public String target(
208227
final CheckableResource credentialsResource = new ProcessOutputResource(credentialsProviderCommand.split("\\s+"));
209228
httpClientConfigurer.addInterceptor(new ResourceBasedAuthorizationInterceptor(credentialsResource));
210229
}
230+
if (StringUtils.hasText(proxyUri)) {
231+
if (StringUtils.isEmpty(proxyPassword) && !StringUtils.isEmpty(proxyUsername)) {
232+
// read password from the command line
233+
proxyPassword = userInput.prompt("Proxy Server Password", "", false);
234+
}
235+
httpClientConfigurer.withProxyCredentials(URI.create(proxyUri), proxyUsername, proxyPassword);
236+
}
211237
this.restTemplate.setRequestFactory(httpClientConfigurer.buildClientHttpRequestFactory());
212238

213239
this.shell.setDataFlowOperations(
@@ -409,7 +435,16 @@ public void onApplicationEvent(ApplicationReadyEvent event) {
409435
// Only invoke if the shell is executing in the same application context as the
410436
// data flow server.
411437
if (!initialized) {
412-
target(this.serverUri, this.userName, this.password, this.credentialsProviderCommand, this.skipSslValidation);
438+
target(
439+
this.serverUri,
440+
this.userName,
441+
this.password,
442+
this.credentialsProviderCommand,
443+
this.skipSslValidation,
444+
this.proxyUri,
445+
this.proxyUsername,
446+
this.proxyPassword
447+
);
413448
}
414449
}
415450

@@ -419,7 +454,16 @@ public void afterPropertiesSet() throws Exception {
419454
// mode.
420455
if (applicationContext != null && !applicationContext.containsBean("streamDefinitionRepository")) {
421456
initialized = true;
422-
target(this.serverUri, this.userName, this.password, this.credentialsProviderCommand, this.skipSslValidation);
457+
target(
458+
this.serverUri,
459+
this.userName,
460+
this.password,
461+
this.credentialsProviderCommand,
462+
this.skipSslValidation,
463+
this.proxyUri,
464+
this.proxyUsername,
465+
this.proxyPassword
466+
);
423467
}
424468
}
425469

spring-cloud-dataflow-shell-core/src/main/resources/usage.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Data Flow Options:
66
OAuth Bearer Token (Access Token prefixed with 'Bearer '),
77
e.g. 'Bearer 12345'), [no default].
88
--dataflow.skip-ssl-validation=<true|false> Accept any SSL certificate (even self-signed) [default: no].
9+
--dataflow.proxy.uri=<PROXY-URI> Address of an optional proxy server to use [no default].
10+
--dataflow.proxy.username=<PROXY-USERNAME> Username of the proxy server (if required by proxy server) [no default].
11+
--dataflow.proxy.password=<PROXY-PASSWORD> Password of the proxy server (if required by proxy server) [no default].
912
--dataflow.mode=<MODE> Server DataFlow mode: 'classic' or 'skipper' modes. [default: classic].
1013
--spring.shell.historySize=<SIZE> Default size of the shell log file [default: 3000].
1114
--spring.shell.commandFile=<FILE> Data Flow Shell executes commands read from the file(s) and then exits.

spring-cloud-dataflow-shell-core/src/test/java/org/springframework/cloud/dataflow/shell/command/ConfigCommandTests.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2017 the original author or authors.
2+
* Copyright 2016-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -120,7 +120,7 @@ public void testApiRevisionMismatch() {
120120

121121
configCommands.onApplicationEvent(null);
122122

123-
final String targetResult = configCommands.target("http://localhost:9393", null, null, null, false);
123+
final String targetResult = configCommands.target("http://localhost:9393", null, null, null, false, null, null, null);
124124
assertThat(targetResult, containsString("Incompatible version of Data Flow server detected"));
125125
}
126126

@@ -171,8 +171,15 @@ public void testDataFlowMode(DataFlowMode shellDataFlowMode, DataFlowMode server
171171
securityInfoResource.setAuthenticationEnabled(false);
172172
when(restTemplate.getForObject(Mockito.any(String.class), Mockito.eq(SecurityInfoResource.class))).thenReturn(securityInfoResource);
173173

174-
final String targetResult = configCommands.target(Target.DEFAULT_TARGET, Target.DEFAULT_USERNAME,
175-
Target.DEFAULT_SPECIFIED_PASSWORD, Target.DEFAULT_CREDENTIALS_PROVIDER_COMMAND, true);
174+
final String targetResult = configCommands.target(
175+
Target.DEFAULT_TARGET,
176+
Target.DEFAULT_USERNAME,
177+
Target.DEFAULT_SPECIFIED_PASSWORD,
178+
Target.DEFAULT_CREDENTIALS_PROVIDER_COMMAND,
179+
true,
180+
Target.DEFAULT_PROXY_URI,
181+
Target.DEFAULT_PROXY_USERNAME,
182+
Target.DEFAULT_PROXY_SPECIFIED_PASSWORD);
176183

177184
System.out.println(targetResult);
178185

0 commit comments

Comments
 (0)