Skip to content

Commit

Permalink
SSL option for Quine OSS, Enterprise, and Novelty
Browse files Browse the repository at this point in the history
* Switch from deprecated java.net.URL constructor to Akka's Uri

The public constructor for java.net.URL is deprecated. Akka's Uri
class works, and has Scala-friendly methods, and generally seems nicer
to work with.

* Determine resolvable URL once, pass that into the Recipe for status
query output. Previously this feature required setting an advertised URL
in the settings, which I'm assuming was an oversight. It now defaults to
a URL based on the bind address if an advertised URL is not set.

* Use helper functions for bind address stuff

Novelty just does its own thing as far as this bind address stuff.

GitOrigin-RevId: b2a1b36c0c4d3f2bce5015a555de9f240b589971
  • Loading branch information
LeifW authored and thatbot-copy[bot] committed Sep 20, 2023
1 parent 9c76634 commit b2bddd0
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 52 deletions.
38 changes: 13 additions & 25 deletions quine/src/main/scala/com/thatdot/quine/app/Main.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.thatdot.quine.app

import java.io.File
import java.net.URL
import java.nio.charset.{Charset, StandardCharsets}
import java.text.NumberFormat

Expand All @@ -14,6 +13,7 @@ import scala.util.{Failure, Success}

import akka.Done
import akka.actor.{ActorSystem, Cancellable, CoordinatedShutdown}
import akka.http.scaladsl.model.Uri
import akka.util.Timeout

import cats.syntax.either._
Expand All @@ -23,7 +23,7 @@ import org.slf4j.LoggerFactory
import pureconfig.ConfigSource
import pureconfig.error.ConfigReaderException

import com.thatdot.quine.app.config.{PersistenceAgentType, PersistenceBuilder, QuineConfig}
import com.thatdot.quine.app.config.{PersistenceAgentType, PersistenceBuilder, QuineConfig, WebServerConfig}
import com.thatdot.quine.app.routes.QuineAppRoutes
import com.thatdot.quine.compiler.cypher.{CypherStandingWiretap, registerUserDefinedProcedure}
import com.thatdot.quine.graph._
Expand Down Expand Up @@ -169,9 +169,14 @@ object Main extends App with LazyLogging {
statusLines.info("Graph is ready")

// The web service is started unless it was disabled.
val bindUrl: Option[URL] = Option.when(config.webserver.enabled)(config.webserver.toURL)
val canonicalUrl: Option[URL] =
Option.when(config.webserver.enabled)(config.webserverAdvertise.map(_.toURL)).flatten
val bindAndResolvableAddresses: Option[(WebServerConfig, Uri)] = Option.when(config.webserver.enabled) {
import config.webserver
// if a canonical URL is configured, use that for presentation (eg logging) purposes. Otherwise, infer
// from the bind URL
webserver -> config.webserverAdvertise.fold(webserver.asResolveableUrl)(
_.overrideHostAndPort(webserver.asResolveableUrl)
)
}

@volatile
var recipeInterpreterTask: Option[Cancellable] = None
Expand All @@ -182,7 +187,7 @@ object Main extends App with LazyLogging {
.onComplete {
case Success(()) =>
recipeInterpreterTask = recipe.map(r =>
RecipeInterpreter(statusLines, r, appState, graph, canonicalUrl)(
RecipeInterpreter(statusLines, r, appState, graph, bindAndResolvableAddresses.map(_._2))(
graph.idProvider
)
)
Expand All @@ -197,26 +202,9 @@ object Main extends App with LazyLogging {

attemptAppLoad()

bindUrl foreach { url =>
// if a canonical URL is configured, use that for presentation (eg logging) purposes. Otherwise, infer
// from the bind URL
val resolvableUrl = canonicalUrl.getOrElse {
// hack: if using the bindURL when host is "0.0.0.0" or "::" (INADDR_ANY and IN6ADDR_ANY's most common
// serialized forms) present the URL as "localhost" to the user. This is necessary because while
// INADDR_ANY as a source address means "bind to all interfaces", it cannot necessarily be used as
// a destination address
val resolvableHost =
if (Set("0.0.0.0", "::").contains(url.getHost)) "localhost" else url.getHost
new java.net.URL(
url.getProtocol,
resolvableHost,
url.getPort,
url.getFile
)
}

bindAndResolvableAddresses foreach { case (bindAddress, resolvableUrl) =>
new QuineAppRoutes(graph, appState, config.loadedConfigJson, resolvableUrl, timeout)
.bindWebServer(interface = url.getHost, port = url.getPort)
.bindWebServer(bindAddress.address.asString, bindAddress.port.asInt, bindAddress.ssl)
.onComplete {
case Success(binding) =>
binding.addToCoordinatedShutdown(hardTerminationDeadline = 30.seconds)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.thatdot.quine.app

import java.lang.System.lineSeparator
import java.net.URL
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicReference

Expand All @@ -11,11 +10,10 @@ import scala.util.control.NonFatal
import scala.util.{Failure, Success}

import akka.actor.Cancellable
import akka.http.scaladsl.model.Uri
import akka.stream.Materializer
import akka.stream.scaladsl.{Keep, Sink}

import com.google.common.net.PercentEscaper

import com.thatdot.quine.app.routes.{IngestStreamState, QueryUiConfigurationState, StandingQueryStore}
import com.thatdot.quine.graph.cypher.{QueryResults, Value}
import com.thatdot.quine.graph.{BaseGraph, CypherOpsGraph}
Expand All @@ -36,7 +34,7 @@ object RecipeInterpreter {
recipe: Recipe,
appState: RecipeState,
graphService: CypherOpsGraph,
quineWebserverUrl: Option[URL]
quineWebserverUri: Option[Uri]
)(implicit idProvider: QuineIdProvider): Cancellable = {
statusLines.info(s"Running Recipe: ${recipe.title}")

Expand Down Expand Up @@ -102,9 +100,8 @@ object RecipeInterpreter {
statusQuery @ StatusQuery(cypherQuery) <- recipe.statusQuery
} {
for {
url <- quineWebserverUrl
escapedQuery = new PercentEscaper("", false).escape(cypherQuery)
} statusLines.info(s"Status query URL is $url#$escapedQuery")
url <- quineWebserverUri
} statusLines.info("Status query URL is " + url.withFragment(cypherQuery))
tasks +:= statusQueryProgressReporter(statusLines, graphService, statusQuery)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
package com.thatdot.quine.app.config

import java.net.URL
import java.io.File
import java.net.InetAddress

import akka.http.scaladsl.model.Uri

import com.thatdot.quine.util.{Host, Port}
sealed abstract class WebserverConfig {

final case class SslConfig(path: File, password: Array[Char])

trait WebServerConfig {
def address: Host
def port: Port
def toURL: URL = new URL("http", address.asString, port.asInt, "")
def ssl: Option[SslConfig]
}
final case class WebServerBindConfig(
address: Host,
port: Port,
enabled: Boolean = true
) extends WebserverConfig
enabled: Boolean = true,
ssl: Option[SslConfig] = (sys.env.get("SSL_KEYSTORE_PATH"), sys.env.get("SSL_KEYSTORE_PASSWORD")) match {
case (None, None) => None
case (Some(path), Some(password)) => Some(SslConfig(new File(path), password.toCharArray))
case (Some(_), None) => sys.error("'SSL_KEYSTORE_PATH' was specified but 'SSL_KEYSTORE_PASSWORD' was not")
case (None, Some(_)) => sys.error("'SSL_KEYSTORE_PASSWORD' was specified but 'SSL_KEYSTORE_PATH' was not")
}
) extends WebServerConfig {

val asResolveableUrl: Uri = {
val bindHost: Uri.Host = Uri.Host(address.asString)
// If the host of the bindUri is set to wildcard (INADDR_ANY and IN6ADDR_ANY) - i.e. "0.0.0.0" or "::"
// present the URL as "localhost" to the user. This is necessary because while
// INADDR_ANY as a source address means "bind to all interfaces", it cannot necessarily be
// used as a destination address
val resolveableHost =
if (bindHost.inetAddresses.head.isAnyLocalAddress)
Uri.Host(InetAddress.getLoopbackAddress)
else
bindHost

Uri(if (ssl.isDefined) "https" else "http", Uri.Authority(resolveableHost, port.asInt))
}
}
final case class WebserverAdvertiseConfig(
address: Host,
port: Port
) extends WebserverConfig
) {
def overrideHostAndPort(uri: Uri): Uri = uri.withHost(address.asString).withPort(port.asInt)
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
package com.thatdot.quine.app.routes

import java.io.{File, FileInputStream}
import java.security.{KeyStore, SecureRandom}
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}

import scala.concurrent.Future
import scala.concurrent.duration.DurationInt
import scala.util.Using

import akka.http.scaladsl.Http
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.model.{HttpCharsets, MediaType, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{Route, StandardRoute}
import akka.http.scaladsl.{ConnectionContext, Http}
import akka.stream.Materializer
import akka.util.Timeout

import com.typesafe.scalalogging.LazyLogging

import com.thatdot.quine.app.config.SslConfig
import com.thatdot.quine.graph.BaseGraph
import com.thatdot.quine.model.QuineIdProvider

object MediaTypes {
val `application/yaml` = MediaType.applicationWithFixedCharset("yaml", HttpCharsets.`UTF-8`, "yaml")
}
object SslHelper {

/** Create an SSL context given the path to a Java keystore and its password
* @param path
* @param password
* @return
*/
def sslContextFromKeystore(path: File, password: Array[Char]): SSLContext = {
val keystore = KeyStore.getInstance(KeyStore.getDefaultType)
Using.resource(new FileInputStream(path))(keystoreFile => keystore.load(keystoreFile, password))
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
keyManagerFactory.init(keystore, password)
val trustManagerFacotry = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
trustManagerFacotry.init(keystore)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFacotry.getTrustManagers, new SecureRandom)
sslContext
}
}
trait BaseAppRoutes extends LazyLogging with endpoints4s.akkahttp.server.Endpoints {

val graph: BaseGraph
Expand Down Expand Up @@ -57,16 +82,22 @@ trait BaseAppRoutes extends LazyLogging with endpoints4s.akkahttp.server.Endpoin
}

/** Bind a webserver to server up the main route */
def bindWebServer(interface: String, port: Int): Future[Http.ServerBinding] = {
implicit val sys = graph.system
val route = mainRoute
Http()
def bindWebServer(interface: String, port: Int, ssl: Option[SslConfig]): Future[Http.ServerBinding] = {
import graph.system
val serverBuilder = Http()(system)
.newServerAt(interface, port)
.adaptSettings(
// See https://doc.akka.io/docs/akka-http/10.0/common/http-model.html#registering-custom-media-types
_.mapWebsocketSettings(_.withPeriodicKeepAliveMaxIdle(10.seconds))
.mapParserSettings(_.withCustomMediaTypes(MediaTypes.`application/yaml`))
)
.bind(route)

ssl
.fold(serverBuilder) { ssl =>
serverBuilder.enableHttps(
ConnectionContext.httpsServer(SslHelper.sslContextFromKeystore(ssl.path, ssl.password))
)
}
.bind(Route.toFunction(mainRoute)(system))
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.thatdot.quine.app.routes

import java.net.URL

import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.server.Route

import endpoints4s.openapi.model._
Expand Down Expand Up @@ -107,7 +106,7 @@ final class QuineAppOpenApiDocs(val idProvider: QuineIdProvider)
*
* @param graph the Quine graph
*/
final case class QuineAppOpenApiDocsRoutes(graph: BaseGraph, url: URL)
final case class QuineAppOpenApiDocsRoutes(graph: BaseGraph, uri: Uri)
extends endpoints4s.akkahttp.server.Endpoints
with endpoints4s.akkahttp.server.JsonEntitiesFromEncodersAndDecoders {

Expand All @@ -118,7 +117,7 @@ final case class QuineAppOpenApiDocsRoutes(graph: BaseGraph, url: URL)
get(path / "docs" / "openapi.json"),
ok(
jsonResponse[endpoints4s.openapi.model.OpenApi](
OpenApiRenderer.stringEncoder(Some(Seq(OpenApiServer(url.toString))))
OpenApiRenderer.stringEncoder(Some(Seq(OpenApiServer(uri.toString))))
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.thatdot.quine.app.routes

import java.net.URL

import scala.util.{Failure, Success, Try}

import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{Directives, Route}
import akka.util.Timeout
Expand Down Expand Up @@ -35,7 +34,7 @@ class QuineAppRoutes(
with StandingQueryStore
with IngestStreamState,
val currentConfig: Json,
val url: URL,
val uri: Uri,
val timeout: Timeout
) extends BaseAppRoutes
with QueryUiRoutesImpl
Expand Down Expand Up @@ -81,7 +80,7 @@ class QuineAppRoutes(
}

/** OpenAPI route */
lazy val openApiRoute: Route = QuineAppOpenApiDocsRoutes(graph, url).route
lazy val openApiRoute: Route = QuineAppOpenApiDocsRoutes(graph, uri).route

/** Rest API route */
lazy val apiRoute: Route = {
Expand Down

0 comments on commit b2bddd0

Please sign in to comment.