diff --git a/core/src/main/java/feign/Client.java b/core/src/main/java/feign/Client.java index 4d4765cf4..fd9d36008 100644 --- a/core/src/main/java/feign/Client.java +++ b/core/src/main/java/feign/Client.java @@ -17,6 +17,9 @@ 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; @@ -24,7 +27,10 @@ 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; @@ -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> headers = new LinkedHashMap<>(); + for (Map.Entry> 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) { @@ -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> headers = new LinkedHashMap>(); - for (Map.Entry> 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; } } } diff --git a/core/src/test/java/feign/client/DefaultClientTest.java b/core/src/test/java/feign/client/DefaultClientTest.java index c42d9b293..5d2c4d272 100644 --- a/core/src/test/java/feign/client/DefaultClientTest.java +++ b/core/src/test/java/feign/client/DefaultClientTest.java @@ -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; @@ -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() { @@ -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); + } } diff --git a/pom.xml b/pom.xml index 88fd7bb5c..487ad964f 100644 --- a/pom.xml +++ b/pom.xml @@ -495,8 +495,11 @@ **/.idea/** **/target/** LICENSE + NOTICE + OSSMETADATA **/*.md bnd.bnd + travis/** src/test/resources/** src/main/resources/**