utils.httpclient.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.utils.http
import akka.Done
import akka.actor.ActorSystem
import akka.http.scaladsl.model.HttpEntity.{ChunkStreamPart, Limitable, SizeLimit}
import akka.http.scaladsl.model.HttpHeader.ParsingResult
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.model.ws.{Message, WebSocketRequest, WebSocketUpgradeResponse}
import akka.http.scaladsl.settings.{ClientConnectionSettings, ConnectionPoolSettings}
import akka.http.scaladsl.util.FastFuture
import akka.http.scaladsl.{ClientTransport, ConnectionContext, Http, HttpsConnectionContext}
import akka.stream.{Attributes, FlowShape, Inlet, Materializer, Outlet, OverflowStrategy, QueueOfferResult}
import akka.stream.scaladsl.{Flow, Sink, Source, SourceQueueWithComplete}
import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler}
import akka.util.ByteString
import com.github.blemale.scaffeine.{Cache, Scaffeine}
import com.google.common.base.Charsets
import com.typesafe.sslconfig.akka.AkkaSSLConfig
import com.typesafe.sslconfig.ssl.SSLConfigSettings
import otoroshi.env.Env
import otoroshi.models.{ClientConfig, Target}
import org.apache.commons.codec.binary.Base64
import otoroshi.gateway.{RequestTimeoutException, Timeout}
import otoroshi.netty.{NettyClientConfig, NettyHttpClient}
import otoroshi.next.models.NgOverflowStrategy
import play.api.Logger
import play.api.libs.json._
import play.api.libs.ws._
import play.api.mvc.MultipartFormData
import play.shaded.ahc.org.asynchttpclient.util.Assertions
import otoroshi.security.IdGenerator
import otoroshi.ssl.{Cert, DynamicSSLEngineProvider}
import otoroshi.utils.cache.types.UnboundedTrieMap
import otoroshi.utils.syntax.implicits._
import reactor.netty.http.client.HttpClient
import java.io.{File, FileOutputStream}
import java.net.{InetAddress, InetSocketAddress, URI}
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}
import javax.net.ssl.SSLContext
import scala.collection.concurrent.TrieMap
import scala.collection.immutable.TreeMap
import scala.concurrent.duration.{Duration, _}
import scala.concurrent.{Await, Future, Promise}
import scala.util.control.NoStackTrace
import scala.util.{Failure, Success, Try}
import scala.xml.{Elem, XML}
case class DNPart(raw: String) {
private val parts = raw.split("=").map(_.trim)
val name = {
val head = parts.head.toLowerCase()
if (head == "sn") {
"surname"
} else {
head
}
}
val value = parts.last
}
case class DN(raw: String) {
val parts = raw.split(",").toSeq.map(_.trim).map(DNPart.apply)
def isEqualsTo(other: DN): Boolean = {
parts.size == other.parts.size && parts.forall(p => other.parts.exists(o => o.name == p.name && o.value == p.value))
}
def stringifyDebug: String = s"DN(${stringify})"
def stringify: String = {
parts
.sortWith((a, b) => a.name.compareTo(b.name) > 0)
.map(p => s"${p.name.toUpperCase()}=${p.value}")
.mkString(", ")
}
}
case class MtlsConfig(
certs: Seq[String] = Seq.empty,
trustedCerts: Seq[String] = Seq.empty,
mtls: Boolean = false,
loose: Boolean = false,
trustAll: Boolean = false
) {
lazy val legit: Boolean = certs.nonEmpty || trustedCerts.nonEmpty || trustAll || loose
lazy val actualCerts: Seq[Cert] = {
certs.flatMap { id =>
DynamicSSLEngineProvider.certificates.get(id) match {
case a @ Some(_) => a
case None =>
val dn = DN(id)
DynamicSSLEngineProvider.certificates.values.toSeq.filter { cert =>
cert.certificate.exists { c =>
val otherDn = DN(c.getSubjectDN.getName)
dn.isEqualsTo(otherDn)
}
} filter { cert =>
cert.from.isBefore(org.joda.time.DateTime.now()) && cert.to.isAfter(org.joda.time.DateTime.now())
} sortWith { (c1, c2) =>
c1.to.compareTo(c2.to) > 0
} headOption
}
}
}
lazy val actualTrustedCerts: Seq[Cert] = {
trustedCerts.flatMap { id =>
DynamicSSLEngineProvider.certificates.get(id) match {
case a @ Some(_) => a
case None =>
val dn = DN(id)
DynamicSSLEngineProvider.certificates.values.toSeq.filter { cert =>
cert.certificate.exists { c =>
val otherDn = DN(c.getSubjectDN.getName)
dn.isEqualsTo(otherDn)
}
} filter { cert =>
cert.from.isBefore(org.joda.time.DateTime.now()) && cert.to.isAfter(org.joda.time.DateTime.now())
} sortWith { (c1, c2) =>
c1.to.compareTo(c2.to) > 0
} headOption
}
}
}
def json: JsValue = MtlsConfig.format.writes(this)
def toJKS(implicit env: Env): (java.io.File, java.io.File, String) = {
val password = IdGenerator.token
val path1 = java.nio.file.Files.createTempFile("oto-kafka-keystore-", ".jks")
val path2 = java.nio.file.Files.createTempFile("oto-kafka-truststore-", ".jks")
val certificates1 = certs.flatMap(DynamicSSLEngineProvider.certificates.get)
val certificates2 = trustedCerts.flatMap(DynamicSSLEngineProvider.certificates.get)
val keystore1 = DynamicSSLEngineProvider.createKeyStore(certificates1)
keystore1.store(new FileOutputStream(path1.toFile), password.toCharArray)
val keystore2 = DynamicSSLEngineProvider.createKeyStore(certificates2)
keystore2.store(new FileOutputStream(path2.toFile), password.toCharArray)
env.lifecycle.addStopHook { () =>
path1.toFile.delete()
path1.toFile.deleteOnExit()
path2.toFile.delete()
path2.toFile.deleteOnExit()
Future.successful(())
}
if (trustedCerts.isEmpty) {
(path1.toFile, path1.toFile, password)
} else {
(path1.toFile, path2.toFile, password)
}
}
}
object MtlsConfig {
val default = MtlsConfig()
def read(opt: Option[JsValue]): MtlsConfig = opt.flatMap(json => format.reads(json).asOpt).getOrElse(default)
val format = new Format[MtlsConfig] {
override def reads(json: JsValue): JsResult[MtlsConfig] =
Try {
MtlsConfig(
certs = (json \ "certs")
.asOpt[Seq[String]]
.orElse((json \ "certId").asOpt[String].map(v => Seq(v)))
.map(_.filter(_.trim.nonEmpty))
.getOrElse(Seq.empty),
trustedCerts = (json \ "trustedCerts")
.asOpt[Seq[String]]
.map(_.filter(_.trim.nonEmpty))
.getOrElse(Seq.empty),
mtls = (json \ "mtls").asOpt[Boolean].orElse((json \ "tls").asOpt[Boolean]).getOrElse(false),
loose = (json \ "loose").asOpt[Boolean].getOrElse(false),
trustAll = (json \ "trustAll").asOpt[Boolean].getOrElse(false)
)
} match {
case Failure(e) => JsError(e.getMessage())
case Success(v) => JsSuccess(v)
}
override def writes(o: MtlsConfig): JsValue =
Json.obj(
"certs" -> JsArray(o.certs.map(JsString.apply)),
"trustedCerts" -> JsArray(o.trustedCerts.map(JsString.apply)),
"mtls" -> o.mtls,
"loose" -> o.loose,
"trustAll" -> o.trustAll
)
}
}
class MtlsWs(chooser: WsClientChooser) {
@inline
def url(url: String, config: MtlsConfig): WSRequest =
config match {
case MtlsConfig(_, _, false, _, _) => chooser.url(url)
case m @ MtlsConfig(_, _, true, _, _) => chooser.urlWithCert(url, Some(m))
// case MtlsConfig(seq, seq2, _, _, _) if (seq ++ seq2).isEmpty => chooser.url(url)
// case MtlsConfig(seq, seq2, false, _, _) if (seq ++ seq2).nonEmpty => chooser.url(url)
// case m @ MtlsConfig(seq, seq2, true, _, _) if (seq ++ seq2).nonEmpty => chooser.urlWithCert(url, Some(m))
}
}
object MtlsWs {
def apply(chooser: WsClientChooser): MtlsWs = new MtlsWs(chooser)
}
object WsClientChooser {
def apply(
standardClient: WSClient,
akkaClient: AkkWsClient,
nettyClient: NettyHttpClient,
// ahcCreator: SSLConfigSettings => WSClient,
fullAkka: Boolean,
env: Env
): WsClientChooser = new WsClientChooser(standardClient, akkaClient, nettyClient, /*ahcCreator, */ fullAkka, env)
}
class WsClientChooser(
standardClient: WSClient,
akkaClient: AkkWsClient,
val nettyClient: NettyHttpClient,
// ahcCreator: SSLConfigSettings => WSClient,
fullAkka: Boolean,
env: Env
) extends WSClient {
private[utils] val logger = Logger("otoroshi-ws-client-chooser")
private[utils] val lastSslConfig = new AtomicReference[SSLConfigSettings](null)
private[utils] val connectionContextHolder = new AtomicReference[WSClient](null)
private val nettyClientConfig = NettyClientConfig.parseFrom(env)
private val enforceNettyOnAkka = nettyClientConfig.enforceAkkaClient
private val enforceAll = nettyClientConfig.enforceAll
// private def getAhcInstance(): WSClient = {
// val currentSslContext = DynamicSSLEngineProvider.sslConfigSettings
// if (currentSslContext != null && !currentSslContext.equals(lastSslConfig.get())) {
// lastSslConfig.set(currentSslContext)
// logger.debug("Building new client instance")
// val client = ahcCreator(currentSslContext)
// connectionContextHolder.set(client)
// }
// connectionContextHolder.get()
// }
def ws[T](
request: WebSocketRequest,
targetOpt: Option[Target],
mtlsConfigOpt: Option[MtlsConfig],
clientFlow: Flow[Message, Message, T],
customizer: ClientConnectionSettings => ClientConnectionSettings
): (Future[WebSocketUpgradeResponse], T) = {
val tlsConfigOpt = targetOpt.map(_.mtlsConfig).orElse(mtlsConfigOpt)
val certs: Seq[Cert] = tlsConfigOpt
.filter(_.mtls)
.toSeq
.flatMap(_.actualCerts)
//.flatMap(DynamicSSLEngineProvider.certificates.get)
val trustedCerts: Seq[Cert] = tlsConfigOpt
.filter(_.mtls)
.toSeq
.flatMap(_.actualTrustedCerts)
// .flatMap(DynamicSSLEngineProvider.certificates.get)
akkaClient.executeWsRequest(
request,
tlsConfigOpt.exists(_.loose),
tlsConfigOpt
.filter(_.mtls)
.exists(_.trustAll),
certs,
trustedCerts,
clientFlow,
customizer
)
}
def url(url: String): WSRequest = {
val protocol = scala.util.Try(url.split(":\\/\\/").apply(0)).getOrElse("http")
urlWithProtocol(protocol, url)
}
def urlMulti(url: String, clientConfig: ClientConfig = ClientConfig()): WSRequest = {
val protocol = scala.util.Try(url.split(":\\/\\/").apply(0)).getOrElse("http")
urlWithProtocol(protocol, url, clientConfig)
}
def akkaUrl(url: String, clientConfig: ClientConfig = ClientConfig()): WSRequest = {
if (enforceNettyOnAkka || enforceAll) {
nettyClient.url(url).withClientConfig(clientConfig)
} else {
new AkkaWsClientRequest(akkaClient, url, None, HttpProtocols.`HTTP/1.1`, clientConfig = clientConfig, env = env)(
akkaClient.mat
)
}
}
// def _urlWithCert(url: String, certs: Seq[String], mtls: Boolean = false, loose: Boolean = false): WSRequest = {
// new AkkaWsClientRequest(akkaClient, url, certId.map(c => Target(
// host = "####",
// mtlsConfig = MtlsConfig(certs, mtls, loose)
// // loose = loose,
// // mtls = mtls,
// // certId = Some(c)
// )), HttpProtocols.`HTTP/1.1`, clientConfig = ClientConfig(), env = env)(
// akkaClient.mat
// )
// }
def urlWithCert(url: String, mtlsConfig: Option[MtlsConfig]): WSRequest = {
if (enforceNettyOnAkka || enforceAll) {
mtlsConfig match {
case None => nettyClient.url(url)
case Some(c) => nettyClient.url(url).withTlsConfig(c)
}
} else {
new AkkaWsClientRequest(
akkaClient,
url,
mtlsConfig.map(c =>
Target(
host = "####",
mtlsConfig = c
)
),
HttpProtocols.`HTTP/1.1`,
clientConfig = ClientConfig(),
env = env
)(
akkaClient.mat
)
}
}
def akkaUrlWithTarget(url: String, target: Target, clientConfig: ClientConfig = ClientConfig()): WSRequest = {
if (enforceNettyOnAkka || enforceAll || target.protocol.isHttp2OrHttp3) {
nettyClient.url(url).withTarget(target).withClientConfig(clientConfig).withProtocol(target.protocol.value)
} else {
new AkkaWsClientRequest(
akkaClient,
url,
Some(target),
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
}
}
def akkaHttp2Url(url: String, clientConfig: ClientConfig = ClientConfig()): WSRequest = {
if (enforceNettyOnAkka || enforceAll) {
nettyClient.url(url).withClientConfig(clientConfig).withProtocol("HTTP/2.0")
} else {
new AkkaWsClientRequest(akkaClient, url, None, HttpProtocols.`HTTP/2.0`, clientConfig = clientConfig, env = env)(
akkaClient.mat
)
}
}
// def ahcUrl(url: String): WSRequest = getAhcInstance().url(url)
def classicUrl(url: String): WSRequest = {
if (enforceAll) {
nettyClient.url(url)
} else {
standardClient.url(url)
}
}
def urlWithTarget(url: String, target: Target, clientConfig: ClientConfig = ClientConfig()): WSRequest = {
val useAkkaHttpClient = env.datastores.globalConfigDataStore.latestSafe.map(_.useAkkaHttpClient).getOrElse(false)
if (useAkkaHttpClient || fullAkka) {
if (enforceNettyOnAkka || enforceAll || target.protocol.isHttp2OrHttp3) {
nettyClient.url(url).withTarget(target).withClientConfig(clientConfig).withProtocol(target.protocol.value)
} else {
new AkkaWsClientRequest(
akkaClient,
url,
Some(target),
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
}
} else {
if (enforceAll || target.protocol.isHttp2OrHttp3) {
nettyClient.url(url).withTarget(target).withClientConfig(clientConfig)
} else {
urlWithProtocol(target.scheme, url, clientConfig)
}
}
}
def urlWithProtocol(protocol: String, url: String, clientConfig: ClientConfig = ClientConfig()): WSRequest = {
val useAkkaHttpClient = env.datastores.globalConfigDataStore.latestSafe.map(_.useAkkaHttpClient).getOrElse(false)
protocol.toLowerCase() match {
case _ if enforceAll =>
nettyClient.url(url).withClientConfig(clientConfig).withProtocol(protocol)
case _ if enforceNettyOnAkka && (useAkkaHttpClient || fullAkka) =>
nettyClient.url(url).withClientConfig(clientConfig).withProtocol(protocol)
case "http" if useAkkaHttpClient || fullAkka =>
new AkkaWsClientRequest(
akkaClient,
url,
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "https" if useAkkaHttpClient || fullAkka =>
new AkkaWsClientRequest(
akkaClient,
url,
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "http" if !useAkkaHttpClient => standardClient.url(url)
case "https" if !useAkkaHttpClient => standardClient.url(url)
case "standard:http" => standardClient.url(url.replace("standard:http://", "http://"))
case "standard:https" => standardClient.url(url.replace("standard:https://", "https://"))
// case "ahc:http" => getAhcInstance().url(url.replace("ahc:http://", "http://"))
// case "ahc:https" => getAhcInstance().url(url.replace("ahc:https://", "https://"))
case "ahc:http" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("ahc:http://", "http://"),
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "ahc:https" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("ahc:https://", "http://"),
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "ahttp" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("ahttp://", "http://"),
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "ahttps" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("ahttps://", "https://"),
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "mtls:https" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("mtls:https://", "https://"),
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "mtls" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("mtls://", "https://"),
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case p if p.startsWith("mtls#") => {
val parts = url.split("://")
val mtlsConfigRaw = parts.apply(0).replace("mtls#", "")
val urlEnds = parts.apply(1)
val (loose, certId) = if (mtlsConfigRaw.contains("loose#")) {
(true, mtlsConfigRaw.replace("loose#", ""))
} else {
(false, mtlsConfigRaw)
}
new AkkaWsClientRequest(
akkaClient,
"https://" + urlEnds,
Some(
Target(
host = urlEnds,
mtlsConfig = MtlsConfig(Seq(certId), Seq.empty, true, loose)
// loose = loose,
// mtls = true,
// certId = Some(certId)
)
),
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
}
case "http2" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("http2://", "http://"),
None,
HttpProtocols.`HTTP/2.0`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case "http2s" =>
new AkkaWsClientRequest(
akkaClient,
url.replace("http2s://", "https://"),
None,
HttpProtocols.`HTTP/2.0`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case _ if useAkkaHttpClient || fullAkka =>
new AkkaWsClientRequest(
akkaClient,
url,
None,
HttpProtocols.`HTTP/1.1`,
clientConfig = clientConfig,
env = env
)(
akkaClient.mat
)
case _ if !(useAkkaHttpClient || fullAkka) => standardClient.url(url)
}
}
override def underlying[T]: T = standardClient.underlying[T]
override def close(): Unit = ()
}
object AkkWsClient {
def cookies(httpResponse: HttpResponse): Seq[WSCookie] = {
httpResponse.headers
.collect { case c: `Set-Cookie` =>
c.cookie
}
.map { c =>
WSCookieWithSameSite(
name = c.name,
value = c.value,
domain = c.domain,
path = c.path,
maxAge = c.maxAge,
secure = c.secure,
httpOnly = c.httpOnly,
sameSite = c.`extension`
.filter(_.startsWith("SameSite="))
.map(_.replace("SameSite=", ""))
.flatMap(play.api.mvc.Cookie.SameSite.parse)
)
}
}
}
case class WSCookieWithSameSite(
name: String,
value: String,
domain: Option[String] = None,
path: Option[String] = None,
maxAge: Option[Long] = None,
secure: Boolean = false,
httpOnly: Boolean = false,
sameSite: Option[play.api.mvc.Cookie.SameSite] = None
) extends WSCookie
/*
// huge workaround for https://github.com/akka/akka-http/issues/92, can be disabled by setting otoroshi.options.manualDnsResolve to false
class CustomLooseHostnameVerifier(mkLogger: LoggerFactory) extends HostnameVerifier {
private val logger = mkLogger(getClass)
private val defaultHostnameVerifier = new NoopHostnameVerifier // new DefaultHostnameVerifier(mkLogger)
override def verify(hostname: String, sslSession: SSLSession): Boolean = {
if (hostname.contains("&")) {
val parts = hostname.split("&")
val actualHost = parts(1)
val hostNameMatches = defaultHostnameVerifier.verify(actualHost, sslSession)
if (!hostNameMatches) {
logger.warn(
s"Hostname verification failed on hostname $actualHost, but the connection was accepted because 'loose' is enabled on service target."
)
}
true
} else {
val hostNameMatches = defaultHostnameVerifier.verify(hostname, sslSession)
if (!hostNameMatches) {
logger.warn(
s"Hostname verification failed on hostname $hostname, but the connection was accepted because 'loose' is enabled on service target."
)
}
true
}
}
}
// huge workaround for https://github.com/akka/akka-http/issues/92, can be disabled by setting otoroshi.options.manualDnsResolve to false
class CustomHostnameVerifier(mkLogger: LoggerFactory) extends HostnameVerifier {
private val logger = mkLogger(getClass)
private val defaultHostnameVerifier = new NoopHostnameVerifier //new DefaultHostnameVerifier(mkLogger)
override def verify(hostname: String, sslSession: SSLSession): Boolean = {
if (hostname.contains("&")) {
val parts = hostname.split("&")
val actualHost = parts(1)
// println(s"verifying ${actualHost} over ${sslSession.getPeerCertificateChain.head.getSubjectDN.getName}")
defaultHostnameVerifier.verify(actualHost, sslSession)
} else {
// println(s"verifying ${hostname} over ${sslSession.getPeerCertificateChain.head.getSubjectDN.getName}")
defaultHostnameVerifier.verify(hostname, sslSession)
}
}
}
*/
object SSLConfigSettingsCustomizer {
implicit class BetterSSLConfigSettings(val sslc: SSLConfigSettings) extends AnyVal {
def callIf(pred: => Boolean, f: SSLConfigSettings => SSLConfigSettings): SSLConfigSettings = {
if (pred) {
f(sslc)
} else {
sslc
}
}
}
}
class AkkWsClient(config: WSClientConfig, env: Env)(implicit system: ActorSystem, materializer: Materializer)
extends WSClient {
val ec = system.dispatcher
val mat = materializer
val client = Http(system)
override def underlying[T]: T = client.asInstanceOf[T]
def url(url: String): WSRequest = {
new AkkaWsClientRequest(this, url, clientConfig = ClientConfig(), targetOpt = None, env = env)
}
override def close(): Unit = Await.ready(client.shutdownAllConnectionPools(), 10.seconds) // AWAIT: valid
private[utils] val logger = Logger("otoroshi-akka-ws-client")
private[utils] val wsClientConfig: WSClientConfig = config
private[utils] val akkaSSLConfig: AkkaSSLConfig = AkkaSSLConfig(system).withSettings(
config.ssl
// huge workaround for https://github.com/akka/akka-http/issues/92, can be disabled by setting otoroshi.options.manualDnsResolve to false
// .callIf(env.manualDnsResolve, _.withHostnameVerifierClass(classOf[CustomHostnameVerifier]))
.withSslParametersConfig(
config.ssl.sslParametersConfig
.withClientAuth(com.typesafe.sslconfig.ssl.ClientAuth.need) // TODO: do we really need that ?
)
.withDefault(false)
)
private[utils] val akkaSSLLooseConfig: AkkaSSLConfig = AkkaSSLConfig(system).withSettings(
config.ssl
// huge workaround for https://github.com/akka/akka-http/issues/92, can be disabled by setting otoroshi.options.manualDnsResolve to false
//.callIf(env.manualDnsResolve, _.withHostnameVerifierClass(classOf[CustomLooseHostnameVerifier]))
.withLoose(config.ssl.loose.withAcceptAnyCertificate(true).withDisableHostnameVerification(true))
.withSslParametersConfig(
config.ssl.sslParametersConfig
.withClientAuth(com.typesafe.sslconfig.ssl.ClientAuth.need) // TODO: do we really need that ?
)
.withDefault(false)
)
private[utils] val lastSslContext = new AtomicReference[SSLContext](null)
private[utils] val connectionContextHolder =
new AtomicReference[HttpsConnectionContext](client.createClientHttpsContext(akkaSSLConfig))
private[utils] val connectionContextLooseHolder =
new AtomicReference[HttpsConnectionContext](connectionContextHolder.get())
// client.validateAndWarnAboutLooseSettings()
private[utils] val clientConnectionSettings: ClientConnectionSettings = ClientConnectionSettings(system)
.withConnectingTimeout(FiniteDuration(config.connectionTimeout._1, config.connectionTimeout._2))
.withIdleTimeout(config.idleTimeout)
//.withUserAgentHeader(Some(`User-Agent`("Otoroshi-akka"))) // config.userAgent.map(_ => `User-Agent`(_)))
private[utils] val connectionPoolSettings: ConnectionPoolSettings = ConnectionPoolSettings(system)
.withConnectionSettings(clientConnectionSettings)
.withMaxRetries(0)
.withIdleTimeout(config.idleTimeout)
private[utils] val singleSslContextCache: Cache[String, SSLContext] = Scaffeine()
.recordStats()
.expireAfterWrite(1.hour)
.maximumSize(1000)
.build()
private[utils] def executeRequest[T](
request: HttpRequest,
loose: Boolean,
trustAll: Boolean,
clientCerts: Seq[Cert],
trustedCerts: Seq[Cert],
clientConfig: ClientConfig,
customizer: ConnectionPoolSettings => ConnectionPoolSettings
): Future[HttpResponse] = {
// TODO: fix warning with
// https://github.com/akka/akka/blob/master/akka-stream/src/main/scala/com/typesafe/sslconfig/akka/AkkaSSLConfig.scala#L83-L109
// https://github.com/lightbend/ssl-config/blob/master/ssl-config-core/src/main/scala/com/typesafe/sslconfig/ssl/SSLContextBuilder.scala#L99-L127
clientCerts match {
case certs if (clientCerts ++ trustedCerts).isEmpty => {
val currentSslContext = DynamicSSLEngineProvider.currentClient
if (currentSslContext != null && !currentSslContext.equals(lastSslContext.get())) {
lastSslContext.set(currentSslContext)
val connectionContext: HttpsConnectionContext =
ConnectionContext.https(currentSslContext, sslConfig = Some(akkaSSLConfig))
val connectionContextLoose: HttpsConnectionContext =
ConnectionContext.https(currentSslContext, sslConfig = Some(akkaSSLLooseConfig))
connectionContextHolder.set(connectionContext)
connectionContextLooseHolder.set(connectionContextLoose)
}
val pool = customizer(connectionPoolSettings).withMaxConnections(512)
val cctx = if (loose) connectionContextLooseHolder.get() else connectionContextHolder.get()
if (clientConfig.cacheConnectionSettings.enabled) {
queueClientRequest(request, pool, cctx, clientConfig.cacheConnectionSettings)
} else {
client.singleRequest(
request,
cctx,
pool
)
}
}
case certs if (clientCerts ++ trustedCerts).nonEmpty => {
if (logger.isDebugEnabled)
logger.debug(
s"Calling ${request.uri} with mTLS context of ${clientCerts.size} client certificates and ${trustedCerts.size} trusted certificates"
)
// logger.info(s"Calling ${request.uri} with mTLS context of ${clientCerts.size} client certificates and ${trustedCerts.size} trusted certificates: ${Json.prettyPrint(Json.obj(
// "clientCerts" -> JsArray(clientCerts.map(c => JsString(c.name + " - " + c.enrich().certificates.head.getSubjectDN.getName))),
// "trustedCerts" -> JsArray(trustedCerts.map(c => JsString(c.name + " - " + c.enrich().certificates.head.getSubjectDN.getName))),
// ))}")
val sslContext = env.metrics.withTimer("otoroshi.core.tls.http-client.single-context-fetch") {
val cacheKey = certs.sortWith((c1, c2) => c1.id.compareTo(c2.id) > 0).map(_.cacheKey).mkString("-")
singleSslContextCache.getOrElse(
cacheKey,
DynamicSSLEngineProvider.setupSslContextFor(certs, trustedCerts, trustAll, client = true, env)
)
}
// val sslContext = DynamicSSLEngineProvider.setupSslContextFor(clientCerts, trustedCerts, trustAll, env)
env.metrics.withTimer("otoroshi.core.tls.http-client.single-context-call") {
val pool = customizer(connectionPoolSettings).withMaxConnections(512)
val cctx = if (loose) {
ConnectionContext.https(sslContext, sslConfig = Some(akkaSSLLooseConfig))
} else {
ConnectionContext.https(sslContext, sslConfig = Some(akkaSSLConfig))
}
if (clientConfig.cacheConnectionSettings.enabled) {
queueClientRequest(request, pool, cctx, clientConfig.cacheConnectionSettings)
} else {
client.singleRequest(request, cctx, pool)
}
}
}
}
}
private val queueCache = new UnboundedTrieMap[String, SourceQueueWithComplete[(HttpRequest, Promise[HttpResponse])]]()
private def getQueue(
request: HttpRequest,
settings: ConnectionPoolSettings,
connectionContext: HttpsConnectionContext,
queueSettings: CacheConnectionSettings
): SourceQueueWithComplete[(HttpRequest, Promise[HttpResponse])] = {
val host = request.uri.authority.host.toString()
val port = request.uri.authority.port
val isHttps = request.uri.scheme.equalsIgnoreCase("https")
val key = s"$host-$port-$isHttps-${queueSettings.queueSize}"
queueCache.getOrElseUpdate(
key, {
if (logger.isDebugEnabled) logger.debug(s"create host connection cache queue for '$key'")
val pool = if (isHttps) {
client.cachedHostConnectionPoolHttps[Promise[HttpResponse]](
host = host,
port = port,
connectionContext = connectionContext,
settings = settings
)
} else {
client.cachedHostConnectionPool[Promise[HttpResponse]](host = host, port = port, settings = settings)
}
Source
.queue[(HttpRequest, Promise[HttpResponse])](queueSettings.queueSize, queueSettings.strategy.toAkka)
.via(pool)
.to(Sink.foreach {
case (Success(resp), p) => p.success(resp)
case (Failure(e), p) => p.failure(e)
})
.run()
}
)
}
private def queueClientRequest(
request: HttpRequest,
settings: ConnectionPoolSettings,
connectionContext: HttpsConnectionContext,
queueSettings: CacheConnectionSettings
): Future[HttpResponse] = {
val queue = getQueue(request, settings, connectionContext, queueSettings)
val responsePromise = Promise[HttpResponse]()
queue
.offer((request, responsePromise))
.flatMap {
case QueueOfferResult.Enqueued => responsePromise.future
case QueueOfferResult.Dropped =>
FastFuture.failed(ClientQueueError("Client queue overflowed. Try again later."))
case QueueOfferResult.Failure(ex) => FastFuture.failed(ClientQueueError(ex.getMessage))
case QueueOfferResult.QueueClosed =>
FastFuture.failed(
ClientQueueError("Client queue was closed (pool shut down) while running the request. Try again later.")
)
}(ec)
}
private[utils] def executeWsRequest[T](
__request: WebSocketRequest,
loose: Boolean,
trustAll: Boolean,
clientCerts: Seq[Cert],
trustedCerts: Seq[Cert],
clientFlow: Flow[Message, Message, T],
customizer: ClientConnectionSettings => ClientConnectionSettings
): (Future[WebSocketUpgradeResponse], T) = {
val request = __request
.applyOnWithPredicate(_.uri.scheme == "http")(r => r.copy(uri = r.uri.copy(scheme = "ws")))
.applyOnWithPredicate(_.uri.scheme == "https")(r => r.copy(uri = r.uri.copy(scheme = "wss")))
.copy(extraHeaders =
__request.extraHeaders
.filterNot(h => h.lowercaseName() == "content-length")
.filterNot(h => h.lowercaseName() == "content-type")
)
clientCerts match {
case certs if (clientCerts ++ trustedCerts).isEmpty => {
val currentSslContext = DynamicSSLEngineProvider.currentClient
if (currentSslContext != null && !currentSslContext.equals(lastSslContext.get())) {
lastSslContext.set(currentSslContext)
val connectionContext: HttpsConnectionContext =
ConnectionContext.https(currentSslContext, sslConfig = Some(akkaSSLConfig))
val connectionContextLoose: HttpsConnectionContext =
ConnectionContext.https(currentSslContext, sslConfig = Some(akkaSSLLooseConfig))
connectionContextHolder.set(connectionContext)
connectionContextLooseHolder.set(connectionContextLoose)
}
client.singleWebSocketRequest(
request = request,
clientFlow = clientFlow,
connectionContext = if (loose) connectionContextLooseHolder.get() else connectionContextHolder.get(),
settings = customizer(ClientConnectionSettings(system))
)(mat)
}
case certs if (clientCerts ++ trustedCerts).nonEmpty => {
if (logger.isDebugEnabled)
logger.debug(s"Calling ws ${request.uri} with mTLS context of ${certs.size} certificates")
val sslContext = env.metrics.withTimer("otoroshi.core.tls.http-client.single-context-fetch") {
val cacheKey = certs.sortWith((c1, c2) => c1.id.compareTo(c2.id) > 0).map(_.cacheKey).mkString("-")
singleSslContextCache.getOrElse(
cacheKey,
DynamicSSLEngineProvider.setupSslContextFor(certs, trustedCerts, trustAll, client = true, env)
)
}
// val sslContext = DynamicSSLEngineProvider.setupSslContextFor(clientCerts, trustedCerts, trustAll, env)
env.metrics.withTimer("otoroshi.core.tls.http-client.single-context-call") {
val cctx = if (loose) {
ConnectionContext.https(sslContext, sslConfig = Some(akkaSSLLooseConfig))
} else {
ConnectionContext.https(sslContext, sslConfig = Some(akkaSSLConfig))
}
client.singleWebSocketRequest(
request = request,
clientFlow = clientFlow,
connectionContext = cctx,
settings = customizer(ClientConnectionSettings(system))
)(mat)
}
}
}
}
}
case class ClientQueueError(message: String) extends RuntimeException(message) with NoStackTrace
case class CacheConnectionSettings(
enabled: Boolean = false,
queueSize: Int = 2048,
strategy: NgOverflowStrategy = NgOverflowStrategy.dropNew
) {
def json: JsValue = {
Json.obj(
"enabled" -> enabled,
"queueSize" -> queueSize
)
}
}
case class AkkWsClientStreamedResponse(
httpResponse: HttpResponse,
underlyingUrl: String,
mat: Materializer,
requestTimeout: FiniteDuration,
env: Env
) extends WSResponse {
lazy val allHeaders: Map[String, Seq[String]] = {
val headers = httpResponse.headers.groupBy(_.name()).mapValues(_.map(_.value())).toSeq ++ Seq(
("Content-Type" -> Seq(contentType))
)
val headz = TreeMap(headers: _*)(CaseInsensitiveOrdered)
val noContentLengthHeader: Boolean =
httpResponse.entity.contentLengthOption.isEmpty /*headz.getIgnoreCase("Content-Length").isEmpty*/
val isContentLengthZero: Boolean = httpResponse.entity.contentLengthOption.contains(
0L
) /*headz.getIgnoreCase("Content-Length").exists(_.contains("0")) || */
val hasChunkedHeader: Boolean = headz
.getIgnoreCase("Transfer-Encoding")
.isDefined && headz.getIgnoreCase("Transfer-Encoding").exists(_.contains("chunked"))
val isChunked: Boolean =
Option(httpResponse.entity.isChunked()).filter(identity) match { // don't know if actually legit ...
case _ if isContentLengthZero => false
case Some(chunked) => chunked
case None if !env.emptyContentLengthIsChunked => hasChunkedHeader // false
case None if env.emptyContentLengthIsChunked && hasChunkedHeader => true
case None if env.emptyContentLengthIsChunked && !hasChunkedHeader && noContentLengthHeader => true
case _ => false
}
val addContentLengthHeaderZero = isContentLengthZero && !noContentLengthHeader
headz
.applyOnIf(isChunked)(_ + ("Transfer-Encoding" -> Seq("chunked")))
.applyOnIf(addContentLengthHeaderZero)(_ + ("Content-Length" -> Seq("0")))
.applyOnIf(!addContentLengthHeaderZero && httpResponse.entity.contentLengthOption.isDefined)(
_ + ("Content-Length" -> httpResponse.entity.contentLengthOption.toSeq.map(_.toString))
)
}
private lazy val _charset: Option[HttpCharset] = httpResponse.entity.contentType.charsetOption
private lazy val _contentType: String = httpResponse.entity.contentType.mediaType
.toString()
.applyOnWithPredicate(_ == "none/none")(_ => "application/octet-stream") + _charset
.map(v => ";charset=" + v.value)
.getOrElse("")
private lazy val _bodyAsBytes: ByteString =
Await.result(
bodyAsSource.runFold(ByteString.empty)(_ ++ _)(mat),
FiniteDuration(10, TimeUnit.MINUTES)
) // AWAIT: valid
private lazy val _bodyAsString: String = _bodyAsBytes.utf8String
private lazy val _bodyAsXml: Elem = XML.loadString(_bodyAsString)
private lazy val _bodyAsJson: JsValue = Json.parse(_bodyAsString)
private lazy val _cookies: Seq[WSCookie] = AkkWsClient.cookies(httpResponse)
def status: Int = httpResponse.status.intValue()
def statusText: String = httpResponse.status.defaultMessage()
def headers: Map[String, Seq[String]] = allHeaders
def underlying[T]: T = httpResponse.asInstanceOf[T]
def bodyAsSource: Source[ByteString, _] = {
if (ClientConfig.logger.isDebugEnabled)
ClientConfig.logger.debug(s"[httpclient] consuming body in ${requestTimeout}")
httpResponse.entity.dataBytes.takeWithin(requestTimeout)
}
override def header(name: String): Option[String] = headerValues(name).headOption
override def headerValues(name: String): Seq[String] = headers.getOrElse(name, Seq.empty)
override def contentType: String = _contentType
override def body[T: BodyReadable]: T =
throw new RuntimeException("Not supported on this WSClient !!! (StreamedResponse.body)")
def body: String = _bodyAsString
def bodyAsBytes: ByteString = _bodyAsBytes
def cookies: Seq[WSCookie] = _cookies
def cookie(name: String): Option[WSCookie] = _cookies.find(_.name == name)
override def xml: Elem = _bodyAsXml
override def json: JsValue = _bodyAsJson
override def uri: URI = new URI(underlyingUrl)
}
case class AkkWsClientRawResponse(httpResponse: HttpResponse, underlyingUrl: String, rawbody: ByteString)
extends WSResponse {
lazy val allHeaders: Map[String, Seq[String]] = {
val headers = httpResponse.headers.groupBy(_.name()).mapValues(_.map(_.value())).toSeq ++ Seq(
("Content-Type" -> Seq(contentType))
) /*++ (if (httpResponse.entity.isChunked()) {
Seq(("Transfer-Encoding" -> Seq("chunked")))
} else {
Seq.empty
})*/
TreeMap(headers: _*)(CaseInsensitiveOrdered)
}
private lazy val _charset: Option[HttpCharset] = httpResponse.entity.contentType.charsetOption
private lazy val _contentType: String = httpResponse.entity.contentType.mediaType
.toString()
.applyOnWithPredicate(_ == "none/none")(_ => "application/octet-stream") + _charset
.map(v => ";charset=" + v.value)
.getOrElse("")
private lazy val _bodyAsBytes: ByteString = rawbody
private lazy val _bodyAsString: String = rawbody.utf8String
private lazy val _bodyAsXml: Elem = XML.loadString(_bodyAsString)
private lazy val _bodyAsJson: JsValue = Json.parse(_bodyAsString)
private lazy val _cookies: Seq[WSCookie] = AkkWsClient.cookies(httpResponse)
def status: Int = httpResponse.status.intValue()
def statusText: String = httpResponse.status.defaultMessage()
def headers: Map[String, Seq[String]] = allHeaders
def underlying[T]: T = httpResponse.asInstanceOf[T]
def bodyAsSource: Source[ByteString, _] = Source.single(rawbody)
override def header(name: String): Option[String] = headerValues(name).headOption
override def headerValues(name: String): Seq[String] = headers.getOrElse(name, Seq.empty)
def body: String = _bodyAsString
def bodyAsBytes: ByteString = _bodyAsBytes
override def xml: Elem = _bodyAsXml
override def json: JsValue = _bodyAsJson
override def contentType: String = _contentType
def cookies: Seq[WSCookie] = _cookies
override def body[T: BodyReadable]: T =
throw new RuntimeException("Not supported on this WSClient !!! (RawResponse.body)")
def cookie(name: String): Option[WSCookie] = _cookies.find(_.name == name)
override def uri: URI = new URI(underlyingUrl)
}
object CaseInsensitiveOrdered extends Ordering[String] {
def compare(x: String, y: String): Int = {
val xl = x.length
val yl = y.length
if (xl < yl) -1 else if (xl > yl) 1 else x.compareToIgnoreCase(y)
}
}
object WSProxyServerUtils {
def isIgnoredForHost(hostname: String, nonProxyHosts: Seq[String]): Boolean = {
Assertions.assertNotNull(hostname, "hostname")
if (nonProxyHosts.nonEmpty) {
val var2: Iterator[_] = nonProxyHosts.iterator
while ({
var2.hasNext
}) {
val nonProxyHost: String = var2.next.asInstanceOf[String]
if (this.matchNonProxyHost(hostname, nonProxyHost)) return true
}
}
false
}
private def matchNonProxyHost(targetHost: String, nonProxyHost: String): Boolean = {
if (nonProxyHost.length > 1) {
if (nonProxyHost.charAt(0) == '*')
return targetHost.regionMatches(
true,
targetHost.length - nonProxyHost.length + 1,
nonProxyHost,
1,
nonProxyHost.length - 1
)
if (nonProxyHost.charAt(nonProxyHost.length - 1) == '*')
return targetHost.regionMatches(true, 0, nonProxyHost, 0, nonProxyHost.length - 1)
}
nonProxyHost.equalsIgnoreCase(targetHost)
}
}
object AkkaWsClientRequest {
val atomicFalse = new AtomicBoolean(false)
}
case class AkkaWsClientRequest(
client: AkkWsClient,
rawUrl: String,
targetOpt: Option[Target],
protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`,
_method: HttpMethod = HttpMethods.GET,
body: WSBody = EmptyBody,
headers: Map[String, Seq[String]] = Map.empty[String, Seq[String]],
requestTimeout: Option[Duration] = None,
proxy: Option[WSProxyServer] = None,
clientConfig: ClientConfig = ClientConfig(),
alreadyFailed: AtomicBoolean = AkkaWsClientRequest.atomicFalse,
env: Env
)(implicit materializer: Materializer)
extends WSRequest {
implicit val ec = client.ec
override type Self = WSRequest
private val _uri = {
val u = Uri(rawUrl)
targetOpt match {
case None => u
case Some(target) => {
target.ipAddress match {
case None => u // TODO: fix it
// huge workaround for https://github.com/akka/akka-http/issues/92, can be disabled by setting otoroshi.options.manualDnsResolve to false
case Some(ipAddress) if env.manualDnsResolve && u.authority.host.isNamedHost() => {
u.copy(
authority = u.authority.copy(
port = target.thePort
// host = akka.http.scaladsl.model.Uri.Host(s"${ipAddress}&${u.authority.host.address()}")
)
)
}
case Some(ipAddress) if !env.manualDnsResolve && u.authority.host.isNamedHost() => {
val addr = InetAddress.getByAddress(u.authority.host.address(), InetAddress.getByName(ipAddress).getAddress)
u.copy(
authority = u.authority.copy(
port = target.thePort,
host = akka.http.scaladsl.model.Uri.Host(addr)
)
)
}
case Some(ipAddress) => {
u.copy(
authority = u.authority.copy(
port = target.thePort,
host = akka.http.scaladsl.model.Uri.Host(InetAddress.getByName(ipAddress))
)
)
}
}
}
}
}
private def customizer: ConnectionPoolSettings => ConnectionPoolSettings = {
val relUri = _uri.toRelative.toString()
val idleTimeout = clientConfig.extractTimeout(relUri, _.idleTimeout, _.idleTimeout)
val connectionTimeout = clientConfig.extractTimeout(relUri, _.connectionTimeout, _.connectionTimeout)
proxy
.filter(p =>
WSProxyServerUtils.isIgnoredForHost(Uri(rawUrl).authority.host.toString(), p.nonProxyHosts.getOrElse(Seq.empty))
)
.map { proxySettings =>
val proxyAddress = InetSocketAddress.createUnresolved(proxySettings.host, proxySettings.port)
val httpsProxyTransport = (proxySettings.principal, proxySettings.password) match {
case (Some(principal), Some(password)) => {
val auth = akka.http.scaladsl.model.headers.BasicHttpCredentials(principal, password)
//val realmBuilder = new Realm.Builder(proxySettings.principal.orNull, proxySettings.password.orNull)
//val scheme: Realm.AuthScheme = proxySettings.protocol.getOrElse("http").toLowerCase(java.util.Locale.ENGLISH) match {
// case "http" | "https" => Realm.AuthScheme.BASIC
// case "kerberos" => Realm.AuthScheme.KERBEROS
// case "ntlm" => Realm.AuthScheme.NTLM
// case "spnego" => Realm.AuthScheme.SPNEGO
// case _ => scala.sys.error("Unrecognized protocol!")
//}
//realmBuilder.setScheme(scheme)
ClientTransport.httpsProxy(proxyAddress, auth)
}
case _ => ClientTransport.httpsProxy(proxyAddress)
}
a: ConnectionPoolSettings => {
if (ClientConfig.logger.isDebugEnabled)
ClientConfig.logger.debug(
s"[httpclient] using idleTimeout: $idleTimeout, connectionTimeout: $connectionTimeout"
)
a.withTransport(httpsProxyTransport)
.withIdleTimeout(idleTimeout)
.withConnectionSettings(
a.connectionSettings
.withTransport(httpsProxyTransport)
.withConnectingTimeout(connectionTimeout)
.withIdleTimeout(idleTimeout)
)
}
} getOrElse { a: ConnectionPoolSettings =>
val maybeIpAddress = targetOpt match {
case None => None
case Some(target) =>
target.ipAddress.map(addr => InetSocketAddress.createUnresolved(addr, target.thePort))
}
if (env.manualDnsResolve && maybeIpAddress.isDefined) {
a.withTransport(ManualResolveTransport.resolveTo(maybeIpAddress.get))
.withIdleTimeout(idleTimeout)
.withConnectionSettings(
a.connectionSettings
.withTransport(ManualResolveTransport.resolveTo(maybeIpAddress.get))
.withConnectingTimeout(connectionTimeout)
.withIdleTimeout(idleTimeout)
)
} else {
a.withIdleTimeout(idleTimeout)
.withConnectionSettings(
a.connectionSettings
.withConnectingTimeout(connectionTimeout)
.withIdleTimeout(idleTimeout)
)
}
}
}
def withMethod(method: String): WSRequest = {
copy(_method = HttpMethods.getForKeyCaseInsensitive(method).getOrElse(HttpMethod.custom(method)))
}
def withHttpHeaders(headers: (String, String)*): WSRequest = {
copy(
headers = headers.foldLeft(this.headers)((m, hdr) =>
if (m.contains(hdr._1)) m.updated(hdr._1, m(hdr._1) :+ hdr._2)
else m + (hdr._1 -> Seq(hdr._2))
)
)
}
def withRequestTimeout(timeout: Duration): Self = {
if (ClientConfig.logger.isDebugEnabled)
ClientConfig.logger.debug(s"[httpclient] setting requestTimeout to ${timeout}")
copy(requestTimeout = Some(timeout))
}
def withFailureIndicator(af: AtomicBoolean): Self = {
copy(alreadyFailed = af)
}
override def withBody[T](body: T)(implicit evidence$1: BodyWritable[T]): WSRequest =
copy(body = evidence$1.transform(body))
override def withHeaders(headers: (String, String)*): WSRequest = withHttpHeaders(headers: _*)
def stream(): Future[WSResponse] = {
val certs: Seq[Cert] = targetOpt
.filter(_.mtlsConfig.mtls)
.toSeq
.flatMap(_.mtlsConfig.actualCerts)
//.flatMap(DynamicSSLEngineProvider.certificates.get)
val trustedCerts: Seq[Cert] = targetOpt
.filter(_.mtlsConfig.mtls)
.toSeq
.flatMap(_.mtlsConfig.actualTrustedCerts)
//.flatMap(DynamicSSLEngineProvider.certificates.get)
val trustAll: Boolean = targetOpt
.filter(_.mtlsConfig.mtls)
.exists(_.mtlsConfig.trustAll)
val req = buildRequest()
val zeTimeout = requestTimeout
.map(v => FiniteDuration(v.toMillis, TimeUnit.MILLISECONDS))
.getOrElse(env.longRequestTimeout) // (FiniteDuration(30, TimeUnit.DAYS)) // yeah that's infinity ...
if (ClientConfig.logger.isDebugEnabled)
ClientConfig.logger.debug(s"[httpclient] stream request with timeout to ${zeTimeout}")
if (ClientConfig.logger.isDebugEnabled) ClientConfig.logger.debug(s"[httpclient] start req")
val failure = Timeout
.timeout(Done, zeTimeout)(client.ec, env.otoroshiScheduler)
.flatMap(_ => FastFuture.failed(RequestTimeoutException))
val start = System.currentTimeMillis()
val reqExec = client
.executeRequest(
req,
targetOpt.exists(_.mtlsConfig.loose),
trustAll,
certs,
trustedCerts,
clientConfig,
customizer
)
.flatMap { resp =>
val remaining = zeTimeout.toMillis - (System.currentTimeMillis() - start)
if (alreadyFailed.get()) {
if (ClientConfig.logger.isDebugEnabled) ClientConfig.logger.debug(s"[httpclient] stream already failed")
resp.entity.discardBytes()
FastFuture.failed(otoroshi.gateway.RequestTimeoutException)
} else if (remaining <= 0) {
if (ClientConfig.logger.isDebugEnabled) ClientConfig.logger.debug(s"[httpclient] got stream resp too late")
resp.entity.discardBytes()
FastFuture.failed(otoroshi.gateway.RequestTimeoutException)
} else {
val remainingTimeout = remaining.millis
if (ClientConfig.logger.isDebugEnabled)
ClientConfig.logger.debug(s"[httpclient] got stream resp - ${remainingTimeout}")
AkkWsClientStreamedResponse(
resp,
rawUrl,
client.mat,
remainingTimeout,
env
).future
}
}
Future.firstCompletedOf(Seq(reqExec, failure))
}
override def execute(method: String): Future[WSResponse] = {
withMethod(method).execute()
}
override def execute(): Future[WSResponse] = {
val certs: Seq[Cert] = targetOpt
.filter(_.mtlsConfig.mtls)
.toSeq
.flatMap(_.mtlsConfig.actualCerts)
//.flatMap(DynamicSSLEngineProvider.certificates.get)
val trustedCerts: Seq[Cert] = targetOpt
.filter(_.mtlsConfig.mtls)
.toSeq
.flatMap(_.mtlsConfig.actualTrustedCerts)
//.flatMap(DynamicSSLEngineProvider.certificates.get)
val trustAll: Boolean = targetOpt
.filter(_.mtlsConfig.mtls)
.exists(_.mtlsConfig.trustAll)
val zeTimeout = requestTimeout
.map(v => FiniteDuration(v.toMillis, TimeUnit.MILLISECONDS))
.getOrElse(env.longRequestTimeout) // (FiniteDuration(30, TimeUnit.DAYS)) // yeah that's infinity ...
val failure = Timeout
.timeout(Done, zeTimeout)(client.ec, env.otoroshiScheduler)
.flatMap(_ => FastFuture.failed(RequestTimeoutException))
val start = System.currentTimeMillis()
val reqExec = client
.executeRequest(
buildRequest(),
targetOpt.exists(_.mtlsConfig.loose),
trustAll,
certs,
trustedCerts,
clientConfig,
customizer
)
.flatMap { response: HttpResponse =>
// FiniteDuration(client.wsClientConfig.requestTimeout._1, client.wsClientConfig.requestTimeout._2)
val remaining = zeTimeout.toMillis - (System.currentTimeMillis() - start)
if (alreadyFailed.get()) {
if (ClientConfig.logger.isDebugEnabled) ClientConfig.logger.debug(s"[httpclient] execute already failed")
response.entity.discardBytes()
FastFuture.failed(otoroshi.gateway.RequestTimeoutException)
} else if (remaining <= 0) {
if (ClientConfig.logger.isDebugEnabled) ClientConfig.logger.debug(s"[httpclient] got resp too late")
response.entity.discardBytes()
FastFuture.failed(otoroshi.gateway.RequestTimeoutException)
} else {
val remainingTimeout = remaining.millis
if (ClientConfig.logger.isDebugEnabled)
ClientConfig.logger.debug(s"[httpclient] got resp - ${remainingTimeout}")
response.entity
.toStrict(remainingTimeout)
.map(a => (response, a))
}
}
.map { case (response: HttpResponse, body: HttpEntity.Strict) =>
AkkWsClientRawResponse(response, rawUrl, body.data)
}
Future.firstCompletedOf(Seq(reqExec, failure))
}
private def realContentType: Option[ContentType] = {
headers
.get(`Content-Type`.name)
.orElse(
headers
.get(`Content-Type`.lowercaseName)
)
.flatMap(_.headOption)
.map { value =>
HttpHeader.parse("Content-Type", value)
}
.flatMap {
// case ParsingResult.Ok(header, _) => Option(header.asInstanceOf[`Content-Type`].contentType)
case ParsingResult.Ok(header, _) =>
header match {
case `Content-Type`(contentType) => contentType.some
case RawHeader(_, value) if value.contains(",") =>
value.split(",").headOption.map(_.trim).map(v => `Content-Type`.parseFromValueString(v)) match {
case Some(Left(errs)) => {
ClientConfig.logger.error(s"Error while parsing request content-type: ${errs}")
None
}
case Some(Right(`Content-Type`(contentType))) => contentType.some
case None => None
}
case RawHeader(_, value) =>
`Content-Type`.parseFromValueString(value) match {
case Left(errs) => {
ClientConfig.logger.error(s"Error while parsing request content-type: ${errs}")
None
}
case Right(`Content-Type`(contentType)) => contentType.some
}
case _ => None
}
case _ => None
}
}
private def realContentLength: Option[Long] = {
headers
.get(`Content-Length`.name)
.orElse(headers.get(`Content-Length`.lowercaseName))
.flatMap(_.headOption)
.map(_.toLong)
}
private def realUserAgent: Option[String] = {
headers
.get(`User-Agent`.name)
.orElse(headers.get(`User-Agent`.lowercaseName))
.flatMap(_.headOption)
}
lazy val (akkaHttpEntity, updatedHeaders) = {
val ct = realContentType.getOrElse(ContentTypes.`application/octet-stream`)
val cl = realContentLength
body match {
case EmptyBody => (HttpEntity.Empty, headers)
case InMemoryBody(bytes) => (HttpEntity(ct, bytes), headers)
// case SourceBody(_) if cl.isDefined && cl.get == 0L => (HttpEntity.Default(ct, 0L, Source.single(ByteString.empty)), headers) // does not work as Default should have length > 0
case SourceBody(_) if cl.isDefined && cl.get == 0L => (HttpEntity.Strict(ct, ByteString.empty), headers)
case SourceBody(bytes) if cl.isDefined => (HttpEntity(ct, cl.get, bytes), headers)
case SourceBody(bytes) => (HttpEntity(ct, bytes), headers)
}
}
def buildRequest(): HttpRequest = {
// val internalUri = Uri(rawUrl)
// val ua = realUserAgent.flatMap(s => Try(`User-Agent`(s)).toOption)
val akkaHeaders: List[HttpHeader] = updatedHeaders
.flatMap { case (key, values) =>
values.distinct.map(value => HttpHeader.parse(key, value))
}
.flatMap {
case ParsingResult.Ok(header, _) => Option(header)
case _ => None
}
.filter { h =>
h.isNot(`Content-Type`.lowercaseName) &&
h.isNot(`Content-Length`.lowercaseName) &&
h.isNot(`Transfer-Encoding`.lowercaseName) &&
//h.isNot(`User-Agent`.lowercaseName) &&
!(h.is(Cookie.lowercaseName) && h.value().trim.isEmpty)
}
.toList // ++ ua
val onInternalApi = updatedHeaders.getIgnoreCase("Host").map(_.last).contains(env.adminApiHost)
val proto = targetOpt.map(_.protocol.asAkka).getOrElse(protocol)
val finalProtocol =
if (proto == HttpProtocols.`HTTP/1.0` && onInternalApi) HttpProtocols.`HTTP/1.1`
else (if (akkaHttpEntity.isChunked() && proto == HttpProtocols.`HTTP/1.0`) HttpProtocols.`HTTP/1.1`
else proto)
HttpRequest(
method = _method,
uri = _uri,
headers = akkaHeaders,
entity = akkaHttpEntity,
protocol = finalProtocol
)
}
///////////
override def withCookies(cookies: WSCookie*): WSRequest = {
if (cookies.nonEmpty) {
val oldCookies = headers.get("Cookie").getOrElse(Seq.empty[String])
val newCookies = oldCookies :+ cookies.toList
.map { c =>
s"${c.name}=${c.value}"
}
.mkString(";")
copy(
headers = headers + ("Cookie" -> newCookies)
)
} else this
}
override lazy val followRedirects: Option[Boolean] = Some(false)
override def withFollowRedirects(follow: Boolean): Self = this
override def method: String = _method.value
override def queryString: Map[String, Seq[String]] = _uri.query().toMultiMap
override def get(): Future[WSResponse] = withMethod("GET").execute()
override def post[T](body: T)(implicit evidence$2: BodyWritable[T]): Future[WSResponse] =
withMethod("POST")
.withBody(evidence$2.transform(body))
.addHttpHeaders("Content-Type" -> evidence$2.contentType)
.execute()
override def post(body: File): Future[WSResponse] =
withMethod("POST")
.withBody(InMemoryBody(ByteString(scala.io.Source.fromFile(body).mkString)))
.addHttpHeaders("Content-Type" -> "application/octet-stream")
.execute()
override def patch[T](body: T)(implicit evidence$3: BodyWritable[T]): Future[WSResponse] =
withMethod("PATCH")
.withBody(evidence$3.transform(body))
.addHttpHeaders("Content-Type" -> evidence$3.contentType)
.execute()
override def patch(body: File): Future[WSResponse] =
withMethod("PATCH")
.withBody(InMemoryBody(ByteString(scala.io.Source.fromFile(body).mkString)))
.addHttpHeaders("Content-Type" -> "application/octet-stream")
.execute()
override def put[T](body: T)(implicit evidence$4: BodyWritable[T]): Future[WSResponse] =
withMethod("PUT")
.withBody(evidence$4.transform(body))
.addHttpHeaders("Content-Type" -> evidence$4.contentType)
.execute()
override def put(body: File): Future[WSResponse] =
withMethod("PUT")
.withBody(InMemoryBody(ByteString(scala.io.Source.fromFile(body).mkString)))
.addHttpHeaders("Content-Type" -> "application/octet-stream")
.execute()
override def delete(): Future[WSResponse] = withMethod("DELETE").execute()
override def head(): Future[WSResponse] = withMethod("HEAD").execute()
override def options(): Future[WSResponse] = withMethod("OPTIONS").execute()
override lazy val url: String = _uri.toString()
override lazy val uri: URI = new URI(_uri.toRelative.toString())
override lazy val contentType: Option[String] = realContentType.map(_.value)
override lazy val cookies: Seq[WSCookie] = {
headers.get("Cookie").map { headers =>
headers.flatMap { header =>
header.split(";").map { value =>
val parts = value.split("=")
DefaultWSCookie(
name = parts(0),
value = parts(1)
)
}
}
} getOrElse Seq.empty
}
override def withQueryString(parameters: (String, String)*): WSRequest = addQueryStringParameters(parameters: _*)
override def withQueryStringParameters(parameters: (String, String)*): WSRequest =
copy(rawUrl = _uri.withQuery(Uri.Query.apply(parameters: _*)).toString())
override def addQueryStringParameters(parameters: (String, String)*): WSRequest = {
val params: Seq[(String, String)] =
_uri.query().toMultiMap.toSeq.flatMap(t => t._2.map(t2 => (t._1, t2))) ++ parameters
copy(rawUrl = _uri.withQuery(Uri.Query.apply(params: _*)).toString())
}
override def withProxyServer(proxyServer: WSProxyServer): WSRequest = copy(proxy = Option(proxyServer))
override def proxyServer: Option[WSProxyServer] = proxy
override def post(body: Source[MultipartFormData.Part[Source[ByteString, _]], _]): Future[WSResponse] =
post[Source[MultipartFormData.Part[Source[ByteString, _]], _]](body)
override def patch(body: Source[MultipartFormData.Part[Source[ByteString, _]], _]): Future[WSResponse] =
patch[Source[MultipartFormData.Part[Source[ByteString, _]], _]](body)
override def put(body: Source[MultipartFormData.Part[Source[ByteString, _]], _]): Future[WSResponse] =
put[Source[MultipartFormData.Part[Source[ByteString, _]], _]](body)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
override def auth: Option[(String, String, WSAuthScheme)] =
throw new RuntimeException("Not supported on this WSClient !!! (Request.auth)")
override def calc: Option[WSSignatureCalculator] =
throw new RuntimeException("Not supported on this WSClient !!! (Request.calc)")
override def virtualHost: Option[String] =
throw new RuntimeException("Not supported on this WSClient !!! (Request.virtualHost)")
override def sign(calc: WSSignatureCalculator): WSRequest =
throw new RuntimeException("Not supported on this WSClient !!! (Request.sign)")
override def withAuth(username: String, password: String, scheme: WSAuthScheme): WSRequest = {
scheme match {
case WSAuthScheme.BASIC =>
addHttpHeaders(
"Authorization" -> s"Basic ${Base64.encodeBase64String(s"${username}:${password}".getBytes(Charsets.UTF_8))}"
)
case _ => throw new RuntimeException("Not supported on this WSClient !!! (Request.withAuth)")
}
}
override def withRequestFilter(filter: WSRequestFilter): WSRequest =
throw new RuntimeException("Not supported on this WSClient !!! (Request.withRequestFilter)")
override def withVirtualHost(vh: String): WSRequest =
throw new RuntimeException("Not supported on this WSClient !!! (Request.withVirtualHost)")
override def withUrl(url: String): WSRequest = copy(rawUrl = url)
}
object Implicits {
private val logger = Logger("otoroshi-http-implicits")
implicit class BetterStandaloneWSRequest[T <: StandaloneWSRequest](val req: T) extends AnyVal {
def withMaybeProxyServer(opt: Option[WSProxyServer]): req.Self = {
opt match {
case Some(proxy) => req.withProxyServer(proxy)
case None => req.asInstanceOf[req.Self]
}
}
def withFailureIndicator(alreadyFailed: AtomicBoolean): req.Self = {
req match {
case areq: AkkaWsClientRequest => areq.withFailureIndicator(alreadyFailed).asInstanceOf[req.Self]
case _ => req.asInstanceOf[req.Self]
}
}
def ignore()(implicit mat: Materializer): req.Self = {
req match {
case httpRequest: AkkaWsClientRequest =>
Try(httpRequest.akkaHttpEntity.dataBytes.runWith(Sink.ignore)(mat)) match {
case Failure(e) => logger.error("Error while discarding request entity bytes ...", e)
case _ => ()
}
req.asInstanceOf[req.Self]
case _ => req.asInstanceOf[req.Self]
}
}
}
implicit class BetterStandaloneWSResponse[T <: StandaloneWSResponse](val resp: T) extends AnyVal {
def contentLength: Option[Long] = {
resp.underlying[Any] match {
case httpResponse: HttpResponse => httpResponse.entity.contentLengthOption
case _ => resp.header("Content-Length").map(_.toLong)
}
}
def contentLengthStr: Option[String] = {
resp.underlying[Any] match {
case httpResponse: HttpResponse => httpResponse.entity.contentLengthOption.map(_.toString)
case _ => resp.header("Content-Length")
}
}
def isChunked(): Option[Boolean] = {
resp.underlying[Any] match {
case httpResponse: HttpResponse => Some(httpResponse.entity.isChunked())
//case responsePublisher: play.shaded.ahc.org.asynchttpclient.netty.handler.StreamedResponsePublisher => {
// val ahc = req.asInstanceOf[play.api.libs.ws.ahc.AhcWSResponse]
// val field = ahc.getClass.getDeclaredField("underlying")
// field.setAccessible(true)
// val sawsr = field.get(ahc).asInstanceOf[play.api.libs.ws.ahc.StreamedResponse]//.asInstanceOf[play.api.libs.ws.ahc.StandaloneAhcWSResponse]
// println(sawsr.headers)
// // val field2 = sawsr.getClass.getField("ahcResponse")
// // field2.setAccessible(true)
// // val response = field2.get(sawsr).asInstanceOf[play.shaded.ahc.org.asynchttpclient.Response]
// // println(response.getHeaders.names())
// //play.shaded.ahc.org.asynchttpclient.netty.handler.
// //HttpHeaders.isTransferEncodingChunked(responsePublisher)
// None
//}
case _ => None
}
}
def ignore()(implicit mat: Materializer): StandaloneWSResponse = {
resp.underlying[Any] match {
case httpResponse: HttpResponse =>
Try(httpResponse.discardEntityBytes()) match {
case Failure(e) => logger.error("Error while discarding entity bytes ...", e)
case _ => ()
}
resp
case _ => resp
}
}
def ignoreIf(predicate: => Boolean)(implicit mat: Materializer): StandaloneWSResponse = {
if (predicate) {
resp.underlying[Any] match {
case httpResponse: HttpResponse =>
Try(httpResponse.discardEntityBytes()) match {
case Failure(e) => logger.error("Error while discarding entity bytes ...", e)
case _ => ()
}
resp
case _ => resp
}
} else {
resp
}
}
}
}
object ManualResolveTransport {
def resolveTo(address: InetSocketAddress): ClientTransport = {
new ClientTransport {
override def connectTo(host: String, port: Int, settings: ClientConnectionSettings)(implicit
system: ActorSystem
): Flow[ByteString, ByteString, Future[Http.OutgoingConnection]] =
ClientTransport.TCP.connectTo(address.getHostString, address.getPort, settings)
}
}
// huge workaround for https://github.com/akka/akka-http/issues/92, can be disabled by setting otoroshi.options.manualDnsResolve to false
// lazy val http: ClientTransport = ManualResolveTransport()
/*
private case class ManualResolveTransport() extends ClientTransport {
def connectTo(host: String, port: Int, settings: ClientConnectionSettings)(
implicit system: ActorSystem
): Flow[ByteString, ByteString, Future[OutgoingConnection]] = {
val inetSocketAddress = if (host.contains("&")) {
val parts = host.split("&")
val ipAddress = parts(0)
val actualHost = parts(1)
new InetSocketAddress(InetAddress.getByAddress(actualHost, InetAddress.getByName(ipAddress).getAddress), port)
} else {
InetSocketAddress.createUnresolved(host, port)
}
Tcp()
.outgoingConnection(
inetSocketAddress,
settings.localAddress,
settings.socketOptions,
halfClose = true,
settings.connectingTimeout,
settings.idleTimeout
)
.mapMaterializedValue(
_.map(tcpConn => OutgoingConnection(tcpConn.localAddress, tcpConn.remoteAddress))(system.dispatcher)
)
}
}*/
//private case class ManualResolveTransport(ipAddress: String) extends ClientTransport {
// def connectTo(host: String, port: Int, settings: ClientConnectionSettings)(implicit system: ActorSystem): Flow[ByteString, ByteString, Future[OutgoingConnection]] =
// Tcp().outgoingConnection(
// new InetSocketAddress(InetAddress.getByAddress(host, InetAddress.getByName(ipAddress).getAddress), port),
// settings.localAddress,
// settings.socketOptions,
// halfClose = true,
// settings.connectingTimeout,
// settings.idleTimeout
// ).mapMaterializedValue(_.map(tcpConn => OutgoingConnection(tcpConn.localAddress, tcpConn.remoteAddress))(system.dispatcher))
//}
//
//private case class ManualResolveTransportTLS(ipAddress: String, sslContext: SSLContext, neg: NegotiateNewSession) extends ClientTransport {
// def connectTo(host: String, port: Int, settings: ClientConnectionSettings)(implicit system: ActorSystem): Flow[ByteString, ByteString, Future[OutgoingConnection]] =
// Tcp().outgoingTlsConnection(
// new InetSocketAddress(InetAddress.getByAddress(host, InetAddress.getByName(ipAddress).getAddress), port),
// sslContext = sslContext,
// negotiateNewSession = neg,
// localAddress = settings.localAddress,
// options = settings.socketOptions,
// connectTimeout = settings.connectingTimeout,
// idleTimeout = settings.idleTimeout
// ).mapMaterializedValue(_.map(tcpConn => OutgoingConnection(tcpConn.localAddress, tcpConn.remoteAddress))(system.dispatcher))
//}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy