diff --git a/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/DefaultWSProxyServer.java b/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/DefaultWSProxyServer.java new file mode 100644 index 00000000..0c128c7c --- /dev/null +++ b/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/DefaultWSProxyServer.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) Lightbend Inc. + */ + +package play.libs.ws.ahc; + +import play.libs.ws.WSProxyServer; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class DefaultWSProxyServer implements WSProxyServer { + private final String host; + private final int port; + private final String protocol; + private final String proxyType; + private final String principal; + private final String password; + private final String ntlmDomain; + private final List nonProxyHosts; + private final String encoding; + + DefaultWSProxyServer(String host, + Integer port, + String protocol, + String proxyType, + String principal, + String password, + String ntlmDomain, + List nonProxyHosts, + String encoding) { + this.host = Objects.requireNonNull(host, "host cannot be null!"); + this.port = Objects.requireNonNull(port, "port cannot be null"); + this.protocol = protocol; + this.proxyType = proxyType; + this.principal = principal; + this.password = password; + this.ntlmDomain = ntlmDomain; + this.nonProxyHosts = nonProxyHosts; + this.encoding = encoding; + } + + @Override + public String getHost() { + return this.host; + } + + @Override + public int getPort() { + return this.port; + } + + @Override + public Optional getProtocol() { + return Optional.ofNullable(this.protocol); + } + + @Override + public Optional getProxyType() { + return Optional.ofNullable(this.proxyType); + } + + @Override + public Optional getPrincipal() { + return Optional.ofNullable(this.principal); + } + + @Override + public Optional getPassword() { + return Optional.ofNullable(this.password); + } + + @Override + public Optional getNtlmDomain() { + return Optional.ofNullable(this.ntlmDomain); + } + + @Override + public Optional getEncoding() { + return Optional.ofNullable(this.encoding); + } + + @Override + public Optional> getNonProxyHosts() { + return Optional.ofNullable(this.nonProxyHosts); + } + + static Builder builder() { + return new Builder(); + } + + static class Builder { + private String host; + private Integer port; + private String protocol; + private String proxyType; + private String principal; + private String password; + private String ntlmDomain; + private List nonProxyHosts; + private String encoding; + + public Builder withHost(String host) { + this.host = host; + return this; + } + + public Builder withPort(int port) { + this.port = port; + return this; + } + + public Builder withProtocol(String protocol) { + this.protocol = protocol; + return this; + } + + public Builder withProxyType(String proxyType) { + this.proxyType = proxyType; + return this; + } + + public Builder withPrincipal(String principal) { + this.principal = principal; + return this; + } + + public Builder withPassword(String password) { + this.password = password; + return this; + } + + public Builder withNtlmDomain(String ntlmDomain) { + this.ntlmDomain = ntlmDomain; + return this; + } + + public Builder withNonProxyHosts(List nonProxyHosts) { + this.nonProxyHosts = nonProxyHosts; + return this; + } + + public Builder withEncoding(String encoding) { + this.encoding = encoding; + return this; + } + + public WSProxyServer build() { + return new DefaultWSProxyServer(host, + port, + protocol, + proxyType, + principal, + password, + ntlmDomain, + nonProxyHosts, + encoding); + } + } +} diff --git a/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StandaloneAhcWSRequest.java b/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StandaloneAhcWSRequest.java index 591aafe9..c76e1df4 100644 --- a/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StandaloneAhcWSRequest.java +++ b/play-ahc-ws-standalone/src/main/java/play/libs/ws/ahc/StandaloneAhcWSRequest.java @@ -26,22 +26,17 @@ import play.shaded.ahc.org.asynchttpclient.RequestBuilder; import play.shaded.ahc.org.asynchttpclient.SignatureCalculator; +import play.shaded.ahc.org.asynchttpclient.proxy.ProxyServer; +import play.shaded.ahc.org.asynchttpclient.proxy.ProxyType; import play.shaded.ahc.org.asynchttpclient.util.HttpUtils; import java.net.MalformedURLException; +import java.net.Proxy; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletionStage; import static java.util.Collections.singletonList; @@ -67,6 +62,8 @@ public class StandaloneAhcWSRequest implements StandaloneWSRequest { private WSAuthInfo auth; private WSSignatureCalculator calculator; + private WSProxyServer proxyServer; + private final StandaloneAhcWSClient client; private final Materializer materializer; @@ -243,6 +240,15 @@ public StandaloneAhcWSRequest setContentType(String contentType) { return addHeader(CONTENT_TYPE.toString(), contentType); } + @Override + public StandaloneWSRequest setProxyServer(WSProxyServer proxyServer) { + if (proxyServer == null) { + throw new IllegalArgumentException("proxyServer must not be null."); + } + this.proxyServer = proxyServer; + return this; + } + @Override public Optional getContentType() { return getHeader(CONTENT_TYPE.toString()); @@ -324,6 +330,11 @@ public Optional getAuth() { return Optional.ofNullable(this.auth); } + @Override + public Optional getProxyServer() { + return Optional.ofNullable(this.proxyServer); + } + @Override public Optional getCalculator() { return Optional.ofNullable(this.calculator); @@ -527,9 +538,52 @@ Request buildRequest() { builder.addCookie(ahcCookie); }); + getProxyServer().ifPresent(ps -> builder.setProxyServer(createProxy(ps))); + return builder.build(); } + private ProxyServer createProxy(WSProxyServer proxyServer) { + String host = proxyServer.getHost(); + int port = proxyServer.getPort(); + ProxyServer.Builder proxyBuilder = new ProxyServer.Builder(host, port); + + proxyServer.getPrincipal().ifPresent(principal -> { + Realm.Builder realmBuilder = new Realm.Builder(principal, proxyServer.getPassword().orElse(null)); + String protocol = proxyServer.getProtocol().orElse("http").toLowerCase(Locale.ENGLISH); + switch (protocol) { + case "http": + case "https": + realmBuilder.setScheme(Realm.AuthScheme.BASIC); + case "kerberos": + realmBuilder.setScheme(Realm.AuthScheme.KERBEROS); + case "ntlm": + realmBuilder.setScheme(Realm.AuthScheme.NTLM); + case "spnego": + realmBuilder.setScheme(Realm.AuthScheme.SPNEGO); + default: + // Default to BASIC rather than throwing an error. + realmBuilder.setScheme(Realm.AuthScheme.BASIC); + } + proxyServer.getEncoding().ifPresent(enc -> realmBuilder.setCharset(Charset.forName(enc))); + proxyServer.getNtlmDomain().ifPresent(realmBuilder::setNtlmDomain); + proxyBuilder.setRealm(realmBuilder); + }); + + String proxyType = proxyServer.getProxyType().orElse("http").toLowerCase(Locale.ENGLISH); + switch (proxyType) { + case "http": + proxyBuilder.setProxyType(ProxyType.HTTP); + case "socksv4": + proxyBuilder.setProxyType(ProxyType.SOCKS_V4); + case "socksv5": + proxyBuilder.setProxyType(ProxyType.SOCKS_V5); + } + + proxyServer.getNonProxyHosts().ifPresent(proxyBuilder::setNonProxyHosts); + return proxyBuilder.build(); + } + private static void addValueTo(Map> map, String name, String value) { final Optional existing = map.keySet().stream().filter(s -> s.equalsIgnoreCase(name)).findAny(); if (existing.isPresent()) { diff --git a/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StandaloneAhcWSRequest.scala b/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StandaloneAhcWSRequest.scala index d93274a8..27884afa 100644 --- a/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StandaloneAhcWSRequest.scala +++ b/play-ahc-ws-standalone/src/main/scala/play/api/libs/ws/ahc/StandaloneAhcWSRequest.scala @@ -17,6 +17,7 @@ import play.shaded.ahc.io.netty.buffer.Unpooled import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders import play.shaded.ahc.org.asynchttpclient.Realm.AuthScheme import play.shaded.ahc.org.asynchttpclient._ +import play.shaded.ahc.org.asynchttpclient.proxy.ProxyType import play.shaded.ahc.org.asynchttpclient.proxy.{ ProxyServer => AHCProxyServer } import play.shaded.ahc.org.asynchttpclient.util.HttpUtils @@ -450,6 +451,16 @@ case class StandaloneAhcWSRequest( proxyBuilder.setRealm(realmBuilder) } + val proxyType = wsProxyServer.proxyType.getOrElse("http").toLowerCase(java.util.Locale.ENGLISH) match { + case "http" => + ProxyType.HTTP + case "socksv4" => + ProxyType.SOCKS_V4 + case "socksv5" => + ProxyType.SOCKS_V5 + } + proxyBuilder.setProxyType(proxyType); + wsProxyServer.nonProxyHosts.foreach { nonProxyHosts => import scala.jdk.CollectionConverters._ proxyBuilder.setNonProxyHosts(nonProxyHosts.asJava) diff --git a/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSRequestSpec.scala b/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSRequestSpec.scala index c6194e7c..e4b4f753 100644 --- a/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSRequestSpec.scala +++ b/play-ahc-ws-standalone/src/test/scala/play/api/libs/ws/ahc/AhcWSRequestSpec.scala @@ -15,7 +15,6 @@ import org.mockito.Mockito import org.specs2.execute.Result import org.specs2.mutable.Specification import org.specs2.specification.AfterAll - import play.api.libs.oauth.ConsumerKey import play.api.libs.oauth.RequestToken import play.api.libs.oauth.OAuthCalculator @@ -24,6 +23,7 @@ import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames import play.shaded.ahc.org.asynchttpclient.Realm.AuthScheme import play.shaded.ahc.org.asynchttpclient.SignatureCalculator import play.shaded.ahc.org.asynchttpclient.Param +import play.shaded.ahc.org.asynchttpclient.proxy.ProxyType import play.shaded.ahc.org.asynchttpclient.{ Request => AHCRequest } import scala.reflect.ClassTag @@ -457,8 +457,10 @@ class AhcWSRequestSpec extends Specification with AfterAll with DefaultBodyReada protocol = Some("https"), host = "localhost", port = 8080, + proxyType = Some("socksv5"), principal = Some("principal"), - password = Some("password") + password = Some("password"), + nonProxyHosts = Some(List("derp")) ) val req: AHCRequest = client .url("http://playframework.com/") @@ -472,6 +474,8 @@ class AhcWSRequestSpec extends Specification with AfterAll with DefaultBodyReada (actual.getRealm.getPrincipal must be).equalTo("principal") (actual.getRealm.getPassword must be).equalTo("password") (actual.getRealm.getScheme must be).equalTo(AuthScheme.BASIC) + (actual.getProxyType must be).equalTo(ProxyType.SOCKS_V5) + (actual.getNonProxyHosts.asScala must contain("derp")) } "support a proxy server with NTLM" in withClient { client => diff --git a/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSRequestSpec.scala b/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSRequestSpec.scala index 24b14fb3..6bb2a401 100644 --- a/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSRequestSpec.scala +++ b/play-ahc-ws-standalone/src/test/scala/play/libs/ws/ahc/AhcWSRequestSpec.scala @@ -13,9 +13,11 @@ import org.specs2.mutable._ import play.libs.oauth.OAuth import play.libs.ws._ import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaderNames +import play.shaded.ahc.org.asynchttpclient.Realm.AuthScheme import play.shaded.ahc.org.asynchttpclient.Request import play.shaded.ahc.org.asynchttpclient.RequestBuilderBase import play.shaded.ahc.org.asynchttpclient.SignatureCalculator +import play.shaded.ahc.org.asynchttpclient.proxy.ProxyType import scala.jdk.CollectionConverters._ import scala.collection.mutable @@ -175,6 +177,36 @@ class AhcWSRequestSpec extends Specification with DefaultBodyReadables with Defa } + "Use a proxy server" in { + val client = StandaloneAhcWSClient.create( + AhcWSClientConfigFactory.forConfig(ConfigFactory.load(), this.getClass.getClassLoader), /*materializer*/ null + ) + val request = new StandaloneAhcWSRequest(client, "http://example.com", /*materializer*/ null) + val proxyServer = DefaultWSProxyServer + .builder() + .withHost("localhost") + .withPort(8080) + .withPrincipal("principal") + .withPassword("password") + .withProxyType("socksv5") + .withNonProxyHosts(java.util.Arrays.asList("derp")) + .build() + + val req = request + .setProxyServer(proxyServer) + .asInstanceOf[StandaloneAhcWSRequest] + .buildRequest() + val actual = req.getProxyServer + + (actual.getHost must be).equalTo("localhost") + (actual.getPort must be).equalTo(8080) + (actual.getRealm.getPrincipal must be).equalTo("principal") + (actual.getRealm.getPassword must be).equalTo("password") + (actual.getRealm.getScheme must be).equalTo(AuthScheme.BASIC) + (actual.getProxyType must be).equalTo(ProxyType.SOCKS_V5) + (actual.getNonProxyHosts.asScala must contain("derp")) + } + "Use a custom signature calculator" in { val client = StandaloneAhcWSClient.create( AhcWSClientConfigFactory.forConfig(ConfigFactory.load(), this.getClass.getClassLoader), /*materializer*/ null diff --git a/play-ws-standalone/src/main/java/play/libs/ws/StandaloneWSRequest.java b/play-ws-standalone/src/main/java/play/libs/ws/StandaloneWSRequest.java index 7e06af07..efb47ed3 100644 --- a/play-ws-standalone/src/main/java/play/libs/ws/StandaloneWSRequest.java +++ b/play-ws-standalone/src/main/java/play/libs/ws/StandaloneWSRequest.java @@ -356,6 +356,14 @@ default StandaloneWSRequest setAuth(String username, String password, WSAuthSche */ StandaloneWSRequest setContentType(String contentType); + /** + * Sets the proxy server. + * + * @param proxyServer the proxy server + * @return the modified WSRequest + */ + StandaloneWSRequest setProxyServer(WSProxyServer proxyServer); + //------------------------------------------------------------------------- // Getters //------------------------------------------------------------------------- @@ -441,6 +449,8 @@ default Optional getScheme() { */ Optional getAuth(); + Optional getProxyServer(); + /** * @return the signature calculator (example: OAuth), or Optional.empty() if none is set. */ diff --git a/play-ws-standalone/src/main/java/play/libs/ws/WSProxyServer.java b/play-ws-standalone/src/main/java/play/libs/ws/WSProxyServer.java new file mode 100644 index 00000000..92ea624b --- /dev/null +++ b/play-ws-standalone/src/main/java/play/libs/ws/WSProxyServer.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) Lightbend Inc. + */ + +package play.libs.ws; + +import java.util.List; +import java.util.Optional; + +public interface WSProxyServer { + /** The hostname of the proxy server. */ + String getHost(); + + /** The port of the proxy server. */ + int getPort(); + + /** The protocol of the proxy server: "kerberos", "ntlm", "https" etc. Defaults to "http" if not specified. */ + Optional getProtocol(); + + /** The proxy type, "http", "socksv4", or "socksv5". Defaults to "http" if not specified. */ + Optional getProxyType(); + + /** The principal (aka username) of the credentials for the proxy server. */ + Optional getPrincipal(); + + /** The password for the credentials for the proxy server. */ + Optional getPassword(); + + Optional getNtlmDomain(); + + /** The realm's charset. */ + Optional getEncoding(); + + Optional> getNonProxyHosts(); +} diff --git a/play-ws-standalone/src/main/scala/play/api/libs/ws/WS.scala b/play-ws-standalone/src/main/scala/play/api/libs/ws/WS.scala index 4628d410..0f6887aa 100644 --- a/play-ws-standalone/src/main/scala/play/api/libs/ws/WS.scala +++ b/play-ws-standalone/src/main/scala/play/api/libs/ws/WS.scala @@ -86,9 +86,12 @@ trait WSProxyServer { /** The port of the proxy server. */ def port: Int - /** The protocol of the proxy server. Use "http" or "https". Defaults to "http" if not specified. */ + /** The protocol of the proxy server: "kerberos", "ntlm", "https" etc. Defaults to "http" if not specified. */ def protocol: Option[String] + /** The proxy type, "http", "socksv4", or "socksv5". Defaults to "http" if not specified. */ + def proxyType: Option[String] + /** The principal (aka username) of the credentials for the proxy server. */ def principal: Option[String] @@ -107,18 +110,13 @@ trait WSProxyServer { * A WS proxy. */ case class DefaultWSProxyServer( - /* The hostname of the proxy server. */ host: String, - /* The port of the proxy server. */ port: Int, - /* The protocol of the proxy server. Use "http" or "https". Defaults to "http" if not specified. */ protocol: Option[String] = None, - /* The principal (aka username) of the credentials for the proxy server. */ + proxyType: Option[String] = None, principal: Option[String] = None, - /* The password for the credentials for the proxy server. */ password: Option[String] = None, ntlmDomain: Option[String] = None, - /* The realm's charset. */ encoding: Option[String] = None, nonProxyHosts: Option[Seq[String]] = None ) extends WSProxyServer