From d6425cda7deeefe54d8a055c0e8e216ce82238df Mon Sep 17 00:00:00 2001 From: brharrington Date: Thu, 18 Mar 2021 20:21:19 -0500 Subject: [PATCH] config setting for multiple ports (#1278) Updates the server config so it is easy to run on multiple ports. SSL config can optionally be provided for a given port for HTTPS. --- atlas-akka/src/main/resources/reference.conf | 16 ++- .../com/netflix/atlas/akka/WebServer.scala | 123 ++++++++++++++++-- .../src/main/resources/log4j2.xml | 2 +- 3 files changed, 128 insertions(+), 13 deletions(-) diff --git a/atlas-akka/src/main/resources/reference.conf b/atlas-akka/src/main/resources/reference.conf index 358520b8a..3d50338c8 100644 --- a/atlas-akka/src/main/resources/reference.conf +++ b/atlas-akka/src/main/resources/reference.conf @@ -74,8 +74,20 @@ atlas.akka { # Name of the actor system name = "atlas" - # Port to use for the web server - port = 7101 + # Ports to use for the web server + ports = [ + { + port = 7101 + secure = false + }, + #{ + # port = 7102 + # secure = true + # ssl-config { + # // See https://lightbend.github.io/ssl-config/KeyStores.html + # } + #} + ] # How long to wait before giving up on bind bind-timeout = 5 seconds diff --git a/atlas-akka/src/main/scala/com/netflix/atlas/akka/WebServer.scala b/atlas-akka/src/main/scala/com/netflix/atlas/akka/WebServer.scala index 07d46e83f..21b04e448 100644 --- a/atlas-akka/src/main/scala/com/netflix/atlas/akka/WebServer.scala +++ b/atlas-akka/src/main/scala/com/netflix/atlas/akka/WebServer.scala @@ -18,8 +18,10 @@ package com.netflix.atlas.akka import javax.inject.Inject import javax.inject.Singleton import akka.actor.ActorSystem +import akka.http.scaladsl.ConnectionContext import akka.http.scaladsl.Http import akka.http.scaladsl.Http.ServerBinding +import akka.http.scaladsl.HttpsConnectionContext import akka.http.scaladsl.server.Route import akka.stream.Materializer import com.netflix.iep.service.AbstractService @@ -27,8 +29,18 @@ import com.netflix.iep.service.ClassFactory import com.netflix.spectator.api.Registry import com.typesafe.config.Config import com.typesafe.scalalogging.StrictLogging +import com.typesafe.sslconfig.ssl.ClientAuth +import com.typesafe.sslconfig.ssl.ConfigSSLContextBuilder +import com.typesafe.sslconfig.ssl.DefaultKeyManagerFactoryWrapper +import com.typesafe.sslconfig.ssl.DefaultTrustManagerFactoryWrapper +import com.typesafe.sslconfig.ssl.SSLConfigFactory +import com.typesafe.sslconfig.ssl.SSLConfigSettings +import com.typesafe.sslconfig.util.LoggerFactory +import com.typesafe.sslconfig.util.NoDepsLogger +import org.slf4j.Logger import scala.concurrent.Await +import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.concurrent.duration.Duration @@ -56,25 +68,116 @@ class WebServer @Inject() ( ) extends AbstractService with StrictLogging { - private implicit val executionContext = system.dispatcher + private implicit val executionContext: ExecutionContext = system.dispatcher - private val port = config.getInt("atlas.akka.port") + private val portConfigs = WebServer.getPortConfigs(config, "atlas.akka.ports") - private var bindingFuture: Future[ServerBinding] = _ + private var bindingFutures: List[Future[ServerBinding]] = Nil protected def startImpl(): Unit = { val handler = new RequestHandler(config, classFactory) - bindingFuture = Http() - .newServerAt("0.0.0.0", port) - .bindFlow(Route.toFlow(handler.routes)) - logger.info(s"started $name on port $port") + val routes = Route.toFlow(handler.routes) + bindingFutures = portConfigs.map { portConfig => + var builder = Http().newServerAt("0.0.0.0", portConfig.port) + if (portConfig.secure) { + builder = builder.enableHttps(portConfig.createConnectionContext) + } + val future = builder.bindFlow(routes) + logger.info(s"started $name on port ${portConfig.port}") + future + } } protected def stopImpl(): Unit = { - if (bindingFuture != null) { - Await.ready(bindingFuture.flatMap(_.unbind()), Duration.Inf) - } + val shutdownFuture = Future.sequence(bindingFutures.map(_.flatMap(_.unbind()))) + Await.ready(shutdownFuture, Duration.Inf) } def actorSystem: ActorSystem = system } + +object WebServer { + + private case class PortConfig( + port: Int, + secure: Boolean, + sslConfigOption: Option[SSLConfigSettings] + ) { + require(!secure || sslConfigOption.isDefined, s"ssl-config is not set for secure port $port") + + private val sslContext = sslConfigOption.map { sslConfig => + val keyManager = new DefaultKeyManagerFactoryWrapper(sslConfig.keyManagerConfig.algorithm) + val trustManager = new DefaultTrustManagerFactoryWrapper( + sslConfig.trustManagerConfig.algorithm + ) + new ConfigSSLContextBuilder(SslLoggerFactory, sslConfig, keyManager, trustManager).build() + }.orNull + + private val clientAuth = sslConfigOption.map(_.sslParametersConfig.clientAuth).orNull + + def createConnectionContext: HttpsConnectionContext = { + ConnectionContext.httpsServer { () => + val sslEngine = sslContext.createSSLEngine() + sslEngine.setUseClientMode(false) + clientAuth match { + case ClientAuth.Default => sslEngine.setNeedClientAuth(true) + case ClientAuth.None => + case ClientAuth.Need => sslEngine.setNeedClientAuth(true) + case ClientAuth.Want => sslEngine.setWantClientAuth(true) + } + sslEngine + } + } + } + + private def getPortConfigs(config: Config, path: String): List[PortConfig] = { + import scala.jdk.CollectionConverters._ + config + .getConfigList(path) + .asScala + .map(toPortConfig) + .toList + } + + private def toPortConfig(config: Config): PortConfig = { + PortConfig( + config.getInt("port"), + config.getBoolean("secure"), + if (config.hasPath("ssl-config")) + Some(SSLConfigFactory.parse(config.getConfig("ssl-config"))) + else + None + ) + } + + /** + * The sslconfig library has its own logging interface to avoid dependencies. Map it + * to slf4j. + */ + private object SslLoggerFactory extends LoggerFactory { + + override def apply(clazz: Class[_]): NoDepsLogger = { + apply(clazz.getName) + } + + override def apply(name: String): NoDepsLogger = { + val logger = org.slf4j.LoggerFactory.getLogger(name) + new SslLogger(logger) + } + } + + private class SslLogger(logger: Logger) extends NoDepsLogger { + + override def isDebugEnabled: Boolean = logger.isDebugEnabled + + override def debug(msg: String): Unit = logger.debug(msg) + + override def info(msg: String): Unit = logger.info(msg) + + override def warn(msg: String): Unit = logger.warn(msg) + + override def error(msg: String): Unit = logger.error(msg) + + override def error(msg: String, throwable: Throwable): Unit = logger.error(msg, throwable) + } +} diff --git a/atlas-standalone/src/main/resources/log4j2.xml b/atlas-standalone/src/main/resources/log4j2.xml index 62569c52e..359b1117a 100644 --- a/atlas-standalone/src/main/resources/log4j2.xml +++ b/atlas-standalone/src/main/resources/log4j2.xml @@ -1,7 +1,7 @@ - %d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5level [%t] %class: %msg%n + %d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5level [%t] %logger: %msg%n