Skip to content

Commit

Permalink
GH-801: Adding support for JDK Proxy (#1045)
Browse files Browse the repository at this point in the history
Fixes #801

Adding a `Proxied` client implementation that extends the `Default`
client allowing for a JDK Proxy, along with explict credential support.
  • Loading branch information
kdavisk6 authored Aug 26, 2019
1 parent 1a4474e commit 6818807
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 35 deletions.
125 changes: 91 additions & 34 deletions core/src/main/java/feign/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@
import static feign.Util.CONTENT_LENGTH;
import static feign.Util.ENCODING_DEFLATE;
import static feign.Util.ENCODING_GZIP;
import static feign.Util.checkArgument;
import static feign.Util.checkNotNull;
import static feign.Util.isNotBlank;
import static java.lang.String.format;

import feign.Request.Options;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -65,9 +71,51 @@ public Response execute(Request request, Options options) throws IOException {
return convertResponse(connection, request);
}

Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();

if (status < 0) {
throw new IOException(
format(
"Invalid status(%s) executing %s %s",
status, connection.getRequestMethod(), connection.getURL()));
}

Map<String, Collection<String>> headers = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null) {
headers.put(field.getKey(), field.getValue());
}
}

Integer length = connection.getContentLength();
if (length == -1) {
length = null;
}
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
return Response.builder()
.status(status)
.reason(reason)
.headers(headers)
.request(request)
.body(stream, length)
.build();
}

public HttpURLConnection getConnection(final URL url) throws IOException {
return (HttpURLConnection) url.openConnection();
}

HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final HttpURLConnection connection =
(HttpURLConnection) new URL(request.url()).openConnection();
final URL url = new URL(request.url());
final HttpURLConnection connection = this.getConnection(url);
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
Expand Down Expand Up @@ -135,43 +183,52 @@ HttpURLConnection convertAndSend(Request request, Options options) throws IOExce
}
return connection;
}
}

/** Client that supports a {@link java.net.Proxy}. */
class Proxied extends Default {

public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
private final Proxy proxy;
private String credentials;

public Proxied(
SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier, Proxy proxy) {
super(sslContextFactory, hostnameVerifier);
checkNotNull(proxy, "a proxy is required.");
this.proxy = proxy;
}

Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();
public Proxied(
SSLSocketFactory sslContextFactory,
HostnameVerifier hostnameVerifier,
Proxy proxy,
String proxyUser,
String proxyPassword) {
this(sslContextFactory, hostnameVerifier, proxy);
checkArgument(isNotBlank(proxyUser), "proxy user is required.");
checkArgument(isNotBlank(proxyPassword), "proxy password is required.");
this.credentials = basic(proxyUser, proxyPassword);
}

if (status < 0) {
throw new IOException(
format(
"Invalid status(%s) executing %s %s",
status, connection.getRequestMethod(), connection.getURL()));
@Override
public HttpURLConnection getConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(this.proxy);
if (isNotBlank(this.credentials)) {
connection.addRequestProperty(PROXY_AUTHORIZATION, this.credentials);
}
return connection;
}

Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null) {
headers.put(field.getKey(), field.getValue());
}
}
public String getCredentials() {
return this.credentials;
}

Integer length = connection.getContentLength();
if (length == -1) {
length = null;
}
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
return Response.builder()
.status(status)
.reason(reason)
.headers(headers)
.request(request)
.body(stream, length)
.build();
private String basic(String username, String password) {
String token = username + ":" + password;
byte[] bytes = token.getBytes(StandardCharsets.ISO_8859_1);
String encoded = Base64.getEncoder().encodeToString(bytes);
return "Basic " + encoded;
}
}
}
45 changes: 44 additions & 1 deletion core/src/test/java/feign/client/DefaultClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,23 @@
*/
package feign.client;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.core.Is.isA;
import static org.junit.Assert.assertEquals;

import feign.Client;
import feign.Client.Proxied;
import feign.Feign;
import feign.Feign.Builder;
import feign.RetryableException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.SocketAddress;
import java.net.URL;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import okhttp3.mockwebserver.MockResponse;
Expand All @@ -31,7 +39,7 @@
/** Tests client-specific behavior, such as ensuring Content-Length is sent when specified. */
public class DefaultClientTest extends AbstractClientTest {

Client disableHostnameVerification =
protected Client disableHostnameVerification =
new Client.Default(
TrustingSSLSocketFactory.get(),
new HostnameVerifier() {
Expand Down Expand Up @@ -104,4 +112,39 @@ public void canOverrideHostnameVerifier() throws IOException, InterruptedExcepti

api.post("foo");
}

private final SocketAddress proxyAddress = new InetSocketAddress("proxy.example.com", 8080);

/**
* Test that the proxy is being used, but don't check the credentials. Credentials can still be
* used, but they must be set using the appropriate system properties and testing that is not what
* we are looking to do here.
*/
@Test
public void canCreateWithImplicitOrNoCredentials() throws Exception {
Proxied proxied =
new Proxied(TrustingSSLSocketFactory.get(), null, new Proxy(Type.HTTP, proxyAddress));
assertThat(proxied).isNotNull();
assertThat(proxied.getCredentials()).isNullOrEmpty();

/* verify that the proxy */
HttpURLConnection connection = proxied.getConnection(new URL("http://www.example.com"));
assertThat(connection).isNotNull().isInstanceOf(HttpURLConnection.class);
}

@Test
public void canCreateWithExplicitCredentials() throws Exception {
Proxied proxied =
new Proxied(
TrustingSSLSocketFactory.get(),
null,
new Proxy(Type.HTTP, proxyAddress),
"user",
"password");
assertThat(proxied).isNotNull();
assertThat(proxied.getCredentials()).isNotBlank();

HttpURLConnection connection = proxied.getConnection(new URL("http://www.example.com"));
assertThat(connection).isNotNull().isInstanceOf(HttpURLConnection.class);
}
}
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,11 @@
<exclude>**/.idea/**</exclude>
<exclude>**/target/**</exclude>
<exclude>LICENSE</exclude>
<exclude>NOTICE</exclude>
<exclude>OSSMETADATA</exclude>
<exclude>**/*.md</exclude>
<exclude>bnd.bnd</exclude>
<exclude>travis/**</exclude>
<exclude>src/test/resources/**</exclude>
<exclude>src/main/resources/**</exclude>
</excludes>
Expand Down

0 comments on commit 6818807

Please sign in to comment.