From cfba3c68875e3918a7897c59f0c8428e6d96783d Mon Sep 17 00:00:00 2001 From: Hubert Plociniczak Date: Thu, 12 Oct 2023 00:03:34 +0200 Subject: [PATCH] Add support for https and wss (#7937) * Add support for https and wss Preliminary support for https and wss. During language server startup we will read the application config and search for the `https` config with necessary env vars set. The configuration supports two modes of creating ssl-context - via PKCS12 format and certificat+private key. Fixes #7839. * Added tests, improved documentation Generic improvements along with actual tests. * lint * more docs + wss support * changelog * Apply suggestions from code review Co-authored-by: Dmitry Bushev * PR comment * typo * lint * make windows line endings happy --------- Co-authored-by: Dmitry Bushev --- CHANGELOG.md | 2 + build.sbt | 12 +- docs/CONTRIBUTING.md | 6 +- .../language-server-http-endpoints.md | 18 ++ docs/language-server/protocol-architecture.md | 4 + .../protocol-project-manager.md | 10 + .../src/main/resources/application.conf | 12 + .../boot/LanguageServerComponent.scala | 53 ++++- .../boot/LanguageServerConfig.scala | 4 + .../enso/languageserver/boot/MainModule.scala | 28 ++- .../http/server/BinaryWebSocketServer.scala | 30 ++- .../launcher/cli/LauncherApplication.scala | 28 ++- .../components/LauncherRunnerSpec.scala | 10 +- .../org/enso/runner/LanguageServerApp.scala | 13 +- .../src/main/scala/org/enso/runner/Main.scala | 32 +++ .../org/enso/jsonrpc/JsonRpcServer.scala | 29 +-- .../enso/jsonrpc/SecureConnectionConfig.scala | 209 ++++++++++++++++++ .../main/scala/org/enso/jsonrpc/Server.scala | 55 +++++ .../enso/jsonrpc/SSLContextBuilderTest.java | 100 +++++++++ .../src/test/resources/example.com.crt | 29 +++ .../src/test/resources/example.com.key | 32 +++ .../src/test/resources/example.com.p12 | Bin 0 -> 4850 bytes .../src/main/resources/application.conf | 3 + .../projectmanager/boot/configuration.scala | 8 +- .../data/LanguageServerSockets.scala | 9 +- .../http/AkkaBasedWebSocketConnection.scala | 31 ++- .../AkkaBasedWebSocketConnectionFactory.scala | 16 +- .../http/WebSocketConnectionFactory.scala | 11 + .../ExecutorWithUnlimitedPool.scala | 16 +- .../LanguageServerBootLoader.scala | 63 ++++-- .../LanguageServerConnectionInfo.scala | 4 +- .../LanguageServerController.scala | 8 +- .../LanguageServerExecutor.scala | 5 +- .../LanguageServerProcess.scala | 10 + .../infrastructure/net/Tcp.scala | 12 +- .../protocol/ProjectManagementApi.scala | 2 + .../requesthandler/ProjectOpenHandler.scala | 14 +- .../projectmanager/ProjectManagementOps.scala | 2 + .../LanguageServerSupervisorSpec.scala | 4 +- .../runner/LanguageServerOptions.scala | 6 +- .../runtimeversionmanager/runner/Runner.scala | 9 +- 41 files changed, 844 insertions(+), 105 deletions(-) create mode 100644 lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/SecureConnectionConfig.scala create mode 100644 lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/Server.scala create mode 100644 lib/scala/json-rpc-server/src/test/java/org/enso/jsonrpc/SSLContextBuilderTest.java create mode 100644 lib/scala/json-rpc-server/src/test/resources/example.com.crt create mode 100644 lib/scala/json-rpc-server/src/test/resources/example.com.key create mode 100644 lib/scala/json-rpc-server/src/test/resources/example.com.p12 diff --git a/CHANGELOG.md b/CHANGELOG.md index 14eebff3e6..7d0152b9d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -978,6 +978,7 @@ - [New `project/status` route for reporting LS state][7801] - [Add Enso-specific assertions][7883] - [Modules can be `private`][7840] +- [HTTPS and WSS support in Language Server][7937] - [Export of non-existing symbols results in error][7960] [3227]: https://github.com/enso-org/enso/pull/3227 @@ -1126,6 +1127,7 @@ [7861]: https://github.com/enso-org/enso/pull/7861 [7883]: https://github.com/enso-org/enso/pull/7883 [7840]: https://github.com/enso-org/enso/pull/7840 +[7937]: https://github.com/enso-org/enso/pull/7937 [7960]: https://github.com/enso-org/enso/pull/7960 # Enso 2.0.0-alpha.18 (2021-10-12) diff --git a/build.sbt b/build.sbt index f0517a459f..cd3920c376 100644 --- a/build.sbt +++ b/build.sbt @@ -484,6 +484,7 @@ val junitIfVersion = "0.13.2" val hamcrestVersion = "1.3" val netbeansApiVersion = "RELEASE180" val fansiVersion = "0.4.0" +val httpComponentsVersion = "4.4.1" // ============================================================================ // === Internal Libraries ===================================================== @@ -951,10 +952,15 @@ lazy val `json-rpc-server` = project libraryDependencies ++= akka ++ logbackTest, libraryDependencies ++= circe, libraryDependencies ++= Seq( - "io.circe" %% "circe-literal" % circeVersion, - "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, + "io.circe" %% "circe-literal" % circeVersion, + "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, akkaTestkit % Test, - "org.scalatest" %% "scalatest" % scalatestVersion % Test + "org.scalatest" %% "scalatest" % scalatestVersion % Test, + "junit" % "junit" % junitVersion % Test, + "com.github.sbt" % "junit-interface" % junitIfVersion % Test, + "org.apache.httpcomponents" % "httpclient" % httpComponentsVersion % Test, + "org.apache.httpcomponents" % "httpcore" % httpComponentsVersion % Test, + "commons-io" % "commons-io" % commonsIoVersion % Test ) ) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 4a99c92108..9299f0886a 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -751,7 +751,7 @@ content root to be provided (`--root-id` and `--path` options). Command-line interface of the runner prints all server options when you execute it with `--help` option. -Below are options uses by the Language Server: +Below are options used by the Language Server: - `--server`: Runs the Language Server - `--root-id `: Content root id. The Language Server chooses one randomly, @@ -763,6 +763,10 @@ Below are options uses by the Language Server: value is 8080. - `--data-port `: Data port for visualization protocol. Default value is 8081. +- `--secure-rpc-port `: (optional) Secure RPC port for processing all + incoming connections. +- `--secure-data-port `: (optional) Secure data port for visualization + protocol. To run the Language Server on 127.0.0.1:8080 type: diff --git a/docs/language-server/language-server-http-endpoints.md b/docs/language-server/language-server-http-endpoints.md index 6aa08845b2..2d0cbfd265 100644 --- a/docs/language-server/language-server-http-endpoints.md +++ b/docs/language-server/language-server-http-endpoints.md @@ -168,3 +168,21 @@ Reset the idle time of the language server. < OK ``` + +# HTTPS endpoints + +Language server can expose HTTPS endpoints when configured appropriately: + +1. Project-manager must be told that project's language server should be started + with https/wss support + - `NETWORK_ENABLE_HTTPS=true` +2. User should provide appropriate secure configuration. Currently supported are + PKCS12 bundle with password and certificate with private key. Depending on + the configuration present, either choice will be sufficient. + +If a project-manager is started with `ENSO_HTTPS_PUBLIC_CERTIFICATE` and +`ENSO_HTTPS_PRIVATE_KEY` env variables, SSL context will be created from a +certificate and a private key, respectively. If a project-manager is started +with `ENSO_HTTPS_PKCS12_PATH` and `ENSO_HTTPS_PKCS12_PASSWORD` env variables, +SSL context will be created from a file in PKCS12 format and a password to it, +respectively. diff --git a/docs/language-server/protocol-architecture.md b/docs/language-server/protocol-architecture.md index 2a46ab97fb..a596e41520 100644 --- a/docs/language-server/protocol-architecture.md +++ b/docs/language-server/protocol-architecture.md @@ -677,6 +677,10 @@ process for spawning and connecting to an engine instance is as follows: data connection, passing its client identifier as it does so. See [`session/initDataConnection`](./protocol-language-server.md#sessioninitdataconnection) below more information. +5. **Secure connections:** The language server can expose secure endpoints + (HTTPS and WSS), when configured appropriately. See + [HTTPS endpoints](./language-server-http-endpoints.md#https-endpoints) for + details. ## Service Connection Teardown diff --git a/docs/language-server/protocol-project-manager.md b/docs/language-server/protocol-project-manager.md index d15d3e52d8..3cff1bba68 100644 --- a/docs/language-server/protocol-project-manager.md +++ b/docs/language-server/protocol-project-manager.md @@ -242,11 +242,21 @@ the action. */ languageServerJsonAddress: IPWithSocket; + /** + * The optional endpoint used for secure JSON-RPC protocol. + */ + languageServerSecureJsonAddress?: IPWithSocket; + /** * The endpoint used for binary protocol. */ languageServerBinaryAddress: IPWithSocket; + /** + * The optional endpoint used for secure binary protocol. + */ + languageServerSecureBinaryAddress?: IPWithSocket; + // The name of the project as it is opened. projectName: String; diff --git a/engine/language-server/src/main/resources/application.conf b/engine/language-server/src/main/resources/application.conf index ceaf17e33f..36fa421883 100644 --- a/engine/language-server/src/main/resources/application.conf +++ b/engine/language-server/src/main/resources/application.conf @@ -9,6 +9,18 @@ akka { websocket.periodic-keep-alive-max-idle = 1 second } } + https { + pkcs12-file = ${?ENSO_HTTPS_PKCS12_PATH} + pkcs12-password = ${?ENSO_HTTPS_PKCS12_PASSWORD} + public-certificate = ${?ENSO_HTTPS_PUBLIC_CERTIFICATE} + public-certificate-algorithm = "X.509" + public-certificate-algorithm = ${?ENSO_HTTPS_PUBLIC_CERTIFICATE_ALGORITHM} + private-key = ${?ENSO_HTTPS_PRIVATE_KEY} + ssl-type = "TLS" + ssl-type = ${?ENSO_HTTPS_SSL} + trust-self-signed = true + trust-self-sgined = ${?ENSO_HTTPS_TRUST_SELF_SIGNED} + } log-dead-letters = 1 log-dead-letters-during-shutdown = off } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala index 800056b2c4..facebd1cd1 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerComponent.scala @@ -45,22 +45,57 @@ class LanguageServerComponent(config: LanguageServerConfig, logLevel: Level) binding <- module.jsonRpcServer.bind(config.interface, config.rpcPort) _ <- Future { logger.debug("Json RPC server initialized.") } } yield binding + val bindSecureJsonServer: Future[Option[Http.ServerBinding]] = { + config.secureRpcPort match { + case Some(port) => + module.jsonRpcServer + .bind(config.interface, port, secure = true) + .map(Some(_)) + case None => + Future.successful(None) + } + } val bindBinaryServer = for { binding <- module.binaryServer.bind(config.interface, config.dataPort) _ <- Future { logger.debug("Binary server initialized.") } } yield binding + + val bindSecureBinaryServer: Future[Option[Http.ServerBinding]] = { + config.secureDataPort match { + case Some(port) => + module.binaryServer + .bind(config.interface, port, secure = true) + .map(Some(_)) + case None => + Future.successful(None) + } + } for { - jsonBinding <- bindJsonServer - binaryBinding <- bindBinaryServer + jsonBinding <- bindJsonServer + secureJsonBinding <- bindSecureJsonServer + binaryBinding <- bindBinaryServer + secureBinaryBinding <- bindSecureBinaryServer _ <- Future { - maybeServerCtx = - Some(ServerContext(sampler, module, jsonBinding, binaryBinding)) + maybeServerCtx = Some( + ServerContext( + sampler, + module, + jsonBinding, + secureJsonBinding, + binaryBinding, + secureBinaryBinding + ) + ) } _ <- Future { logger.info( - s"Started server at json:${config.interface}:${config.rpcPort}, " + - s"binary:${config.interface}:${config.dataPort}" + s"Started server at json:${config.interface}${config.rpcPort}, ${config.secureRpcPort + .map(p => s"secure-jsons:${config.interface}$p") + .getOrElse("")}, " + + s"binary:${config.interface}:${config.dataPort}${config.secureDataPort + .map(p => s", secure-binary:${config.interface}$p") + .getOrElse("")}" ) } } yield ComponentStarted @@ -156,13 +191,17 @@ object LanguageServerComponent { * @param sampler a sampler gathering the application performance statistics * @param mainModule a main module containing all components of the server * @param jsonBinding a http binding for rpc protocol + * @param secureJsonBinding an optional https binding for rpc protocol * @param binaryBinding a http binding for data protocol + * @param secureBinaryBinding an optional https binding for data protocol */ case class ServerContext( sampler: MethodsSampler, mainModule: MainModule, jsonBinding: Http.ServerBinding, - binaryBinding: Http.ServerBinding + secureJsonBinding: Option[Http.ServerBinding], + binaryBinding: Http.ServerBinding, + secureBinaryBinding: Option[Http.ServerBinding] ) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerConfig.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerConfig.scala index 6fa4a87a84..90ffd88165 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerConfig.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/LanguageServerConfig.scala @@ -8,7 +8,9 @@ import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} * * @param interface a interface that the server listen to * @param rpcPort a rpc port that the server listen to + * @param secureRpcPort an optional secure rpc port that the server listen to * @param dataPort a data port that the server listen to + * @param secureDataPort an optional secure data port that the server listen to * @param contentRootUuid an id of content root * @param contentRootPath a path to the content root * @param profilingConfig an application profiling configuration @@ -17,7 +19,9 @@ import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} case class LanguageServerConfig( interface: String, rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], contentRootUuid: UUID, contentRootPath: String, profilingConfig: ProfilingConfig, diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index 0bfe094dc5..5a56469efa 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -2,6 +2,7 @@ package org.enso.languageserver.boot import akka.actor.ActorSystem import buildinfo.Info +import com.typesafe.config.ConfigFactory import org.enso.distribution.locking.{ ResourceManager, ThreadSafeFileLockManager @@ -10,7 +11,7 @@ import org.enso.distribution.{DistributionManager, Environment, LanguageHome} import org.enso.editions.EditionResolver import org.enso.editions.updater.EditionManager import org.enso.filewatcher.WatcherAdapterFactory -import org.enso.jsonrpc.JsonRpcServer +import org.enso.jsonrpc.{JsonRpcServer, SecureConnectionConfig} import org.enso.languageserver.capability.CapabilityRouter import org.enso.languageserver.data._ import org.enso.languageserver.effect @@ -455,12 +456,23 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) { jsonRpcControllerFactory ) + val secureConfig = SecureConnectionConfig + .fromApplicationConfig(applicationConfig()) + .fold( + v => v.flatMap(msg => { log.warn(s"invalid secure config: $msg"); None }), + Some(_) + ) + val jsonRpcServer = new JsonRpcServer( jsonRpcProtocolFactory, jsonRpcControllerFactory, JsonRpcServer - .Config(outgoingBufferSize = 10000, lazyMessageTimeout = 10.seconds), + .Config( + outgoingBufferSize = 10000, + lazyMessageTimeout = 10.seconds, + secureConfig = secureConfig + ), List(healthCheckEndpoint, idlenessEndpoint) ) log.trace("Created JSON RPC Server [{}].", jsonRpcServer) @@ -472,7 +484,8 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) { new BinaryConnectionControllerFactory(fileManager), BinaryWebSocketServer.Config( outgoingBufferSize = 100, - lazyMessageTimeout = 10.seconds + lazyMessageTimeout = 10.seconds, + secureConfig = secureConfig ) ) log.trace("Created Binary WebSocket Server [{}].", binaryServer) @@ -488,4 +501,13 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: Level) { context.close() log.info("Closed Language Server main module.") } + + private def applicationConfig(): com.typesafe.config.Config = { + val empty = ConfigFactory.empty().atPath("akka.https") + ConfigFactory + .load() + .withFallback(empty) + .getConfig("akka") + .getConfig("https") + } } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/http/server/BinaryWebSocketServer.scala b/engine/language-server/src/main/scala/org/enso/languageserver/http/server/BinaryWebSocketServer.scala index 94b6177818..c7dfaeee3c 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/http/server/BinaryWebSocketServer.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/http/server/BinaryWebSocketServer.scala @@ -2,7 +2,6 @@ package org.enso.languageserver.http.server import akka.NotUsed import akka.actor.{ActorRef, ActorSystem} -import akka.http.scaladsl.Http import akka.http.scaladsl.model.RemoteAddress import akka.http.scaladsl.model.StatusCodes.InternalServerError import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage} @@ -12,6 +11,7 @@ import akka.stream.scaladsl.{Flow, Sink, Source} import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy} import akka.util.ByteString import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{SecureConnectionConfig, Server} import org.enso.languageserver.http.server.BinaryWebSocketControlProtocol.{ CloseConnection, ConnectionClosed, @@ -26,7 +26,7 @@ import org.enso.languageserver.util.binary.{ } import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.ExecutionContext /** A web socket server using a binary protocol. * @@ -49,7 +49,8 @@ class BinaryWebSocketServer[A, B]( )( implicit val system: ActorSystem, implicit val materializer: Materializer -) extends LazyLogging { +) extends Server + with LazyLogging { implicit val ec: ExecutionContext = system.dispatcher @@ -67,18 +68,6 @@ class BinaryWebSocketServer[A, B]( } } - /** Binds this server instance to a given port and interface, allowing - * future connections. - * - * @param interface the interface to bind to. - * @param port the port to bind to. - * @return a representation of the binding state of the server. - */ - def bind(interface: String, port: Int): Future[Http.ServerBinding] = - Http() - .newServerAt(interface, port) - .bind(route) - private def newConnection( ip: RemoteAddress.IP ): Flow[Message, Message, NotUsed] = { @@ -148,6 +137,10 @@ class BinaryWebSocketServer[A, B]( } } + override protected def serverRoute(port: Int): Route = route + + override protected def secureConfig(): Option[SecureConnectionConfig] = + config.secureConfig } object BinaryWebSocketServer { @@ -163,6 +156,7 @@ object BinaryWebSocketServer { case class Config( outgoingBufferSize: Int, lazyMessageTimeout: FiniteDuration, + secureConfig: Option[SecureConnectionConfig], path: String = "" ) @@ -173,6 +167,10 @@ object BinaryWebSocketServer { * @return a default config. */ def default: Config = - Config(outgoingBufferSize = 10, lazyMessageTimeout = 10.seconds) + Config( + outgoingBufferSize = 10, + lazyMessageTimeout = 10.seconds, + secureConfig = None + ) } } diff --git a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala index 5c1d4266f6..8b018733a3 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala @@ -206,6 +206,13 @@ object LauncherApplication { "RPC port for processing all incoming connections. Defaults to 8080." ) .withDefault(8080) + val secureRpcPort = + Opts + .optionalParameter[Int]( + "secure-rpc-port", + "SECURE_RPC_PORT", + "Secure RPC port for processing all incoming connections." + ) val dataPort = Opts .optionalParameter[Int]( @@ -214,13 +221,22 @@ object LauncherApplication { "Data port for visualization protocol. Defaults to 8081." ) .withDefault(8081) + val secureDataPort = + Opts + .optionalParameter[Int]( + "secure-data-port", + "SECURE_DATA_PORT", + "Secure data port for visualization protocol." + ) val additionalArgs = Opts.additionalArguments() ( rootId, path, interface, rpcPort, + secureRpcPort, dataPort, + secureDataPort, versionOverride, engineLogLevel, systemJVMOverride, @@ -232,7 +248,9 @@ object LauncherApplication { path, interface, rpcPort, + secureRpcPort, dataPort, + secureDataPort, versionOverride, engineLogLevel, systemJVMOverride, @@ -241,10 +259,12 @@ object LauncherApplication { ) => (config: Config) => Launcher(config).runLanguageServer( options = LanguageServerOptions( - rootId = rootId, - interface = interface, - rpcPort = rpcPort, - dataPort = dataPort + rootId = rootId, + interface = interface, + rpcPort = rpcPort, + secureRpcPort = secureRpcPort, + dataPort = dataPort, + secureDataPort = secureDataPort ), contentRoot = path, versionOverride = versionOverride, diff --git a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala index 5745cc2ee7..578864b78d 100644 --- a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala +++ b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala @@ -286,10 +286,12 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest with FlakySpec { newProject("test", projectPath, version) val options = LanguageServerOptions( - rootId = UUID.randomUUID(), - interface = "127.0.0.2", - rpcPort = 1234, - dataPort = 4321 + rootId = UUID.randomUUID(), + interface = "127.0.0.2", + rpcPort = 1234, + secureRpcPort = None, + dataPort = 4321, + secureDataPort = None ) val runSettings = runner .languageServer( diff --git a/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala b/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala index adeada4396..036defcf56 100644 --- a/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala +++ b/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala @@ -1,5 +1,6 @@ package org.enso.runner +import com.typesafe.scalalogging.Logger import org.enso.languageserver.boot.{ LanguageServerComponent, LanguageServerConfig @@ -7,7 +8,6 @@ import org.enso.languageserver.boot.{ import org.slf4j.event.Level import java.util.concurrent.Semaphore - import scala.concurrent.{Await, ExecutionContext, Future} import scala.concurrent.duration._ import scala.io.StdIn @@ -16,7 +16,8 @@ import scala.io.StdIn */ object LanguageServerApp { - private val semaphore = new Semaphore(1) + private val semaphore = new Semaphore(1) + private lazy val logger = Logger[LanguageServerApp.type] /** Runs a Language Server * @@ -31,7 +32,7 @@ object LanguageServerApp { ): Unit = { val server = new LanguageServerComponent(config, logLevel) Runtime.getRuntime.addShutdownHook(new Thread(() => { - stop(server)(config.computeExecutionContext) + stop(server, "shutdown hook")(config.computeExecutionContext) })) Await.result(server.start(), 1.minute) if (deamonize) { @@ -41,7 +42,7 @@ object LanguageServerApp { } } else { StdIn.readLine() - stop(server)(config.computeExecutionContext) + stop(server, "stopped by the user")(config.computeExecutionContext) } } @@ -51,8 +52,10 @@ object LanguageServerApp { * @param ec the execution context */ private def stop( - server: LanguageServerComponent + server: LanguageServerComponent, + reason: String )(implicit ec: ExecutionContext): Unit = { + logger.info("Stopping Language Server: {}", reason) Await.ready(synchronize(server.stop()), 40.seconds) } diff --git a/engine/runner/src/main/scala/org/enso/runner/Main.scala b/engine/runner/src/main/scala/org/enso/runner/Main.scala index 5a802b1df9..877b5a87df 100644 --- a/engine/runner/src/main/scala/org/enso/runner/Main.scala +++ b/engine/runner/src/main/scala/org/enso/runner/Main.scala @@ -57,6 +57,8 @@ object Main { private val INTERFACE_OPTION = "interface" private val RPC_PORT_OPTION = "rpc-port" private val DATA_PORT_OPTION = "data-port" + private val SECURE_RPC_PORT_OPTION = "secure-rpc-port" + private val SECURE_DATA_PORT_OPTION = "secure-data-port" private val ROOT_ID_OPTION = "root-id" private val ROOT_PATH_OPTION = "path" private val IN_PROJECT_OPTION = "in-project" @@ -220,6 +222,13 @@ object Main { .argName("rpc-port") .desc("RPC port for processing all incoming connections") .build() + val secureRpcPortOption = CliOption.builder + .longOpt(SECURE_RPC_PORT_OPTION) + .hasArg(true) + .numberOfArgs(1) + .argName("rpc-port") + .desc("A secure RPC port for processing all incoming connections") + .build() val dataPortOption = CliOption.builder .longOpt(DATA_PORT_OPTION) .hasArg(true) @@ -227,6 +236,13 @@ object Main { .argName("data-port") .desc("Data port for visualization protocol") .build() + val secureDataPortOption = CliOption.builder + .longOpt(SECURE_DATA_PORT_OPTION) + .hasArg(true) + .numberOfArgs(1) + .argName("data-port") + .desc("A secure data port for visualization protocol") + .build() val uuidOption = CliOption.builder .hasArg(true) .numberOfArgs(1) @@ -412,6 +428,8 @@ object Main { .addOption(interfaceOption) .addOption(rpcPortOption) .addOption(dataPortOption) + .addOption(secureRpcPortOption) + .addOption(secureDataPortOption) .addOption(uuidOption) .addOption(pathOption) .addOption(inProjectOption) @@ -958,6 +976,18 @@ object Main { dataPort <- Either .catchNonFatal(dataPortStr.toInt) .leftMap(_ => "Port must be integer") + secureRpcPortStr = Option(line.getOptionValue(SECURE_RPC_PORT_OPTION)) + .map(Some(_)) + .getOrElse(None) + secureRpcPort <- Either + .catchNonFatal(secureRpcPortStr.map(_.toInt)) + .leftMap(_ => "Port must be integer") + secureDataPortStr = Option(line.getOptionValue(SECURE_DATA_PORT_OPTION)) + .map(Some(_)) + .getOrElse(None) + secureDataPort <- Either + .catchNonFatal(secureDataPortStr.map(_.toInt)) + .leftMap(_ => "Port must be integer") profilingPathStr = Option(line.getOptionValue(LANGUAGE_SERVER_PROFILING_PATH)) profilingPath <- Either @@ -979,7 +1009,9 @@ object Main { } yield boot.LanguageServerConfig( interface, rpcPort, + secureRpcPort, dataPort, + secureDataPort, rootId, rootPath, ProfilingConfig(profilingEventsLogPath, profilingPath, profilingTime), diff --git a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala index 4d3e6258f4..4040066989 100644 --- a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala +++ b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala @@ -2,7 +2,6 @@ package org.enso.jsonrpc import akka.NotUsed import akka.actor.{ActorRef, ActorSystem, Props} -import akka.http.scaladsl.Http import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route @@ -11,9 +10,8 @@ import akka.stream.{Materializer, OverflowStrategy} import com.typesafe.scalalogging.LazyLogging import java.util.UUID - import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.ExecutionContext /** Exposes a multi-client JSON RPC Server instance over WebSocket connections. * @@ -32,7 +30,8 @@ class JsonRpcServer( )( implicit val system: ActorSystem, implicit val materializer: Materializer -) extends LazyLogging { +) extends Server + with LazyLogging { implicit val ec: ExecutionContext = system.dispatcher @@ -92,7 +91,7 @@ class JsonRpcServer( Flow.fromSinkAndSource(incomingMessages, outgoingMessages) } - private def route(port: Int): Route = { + override protected def serverRoute(port: Int): Route = { val webSocketEndpoint = path(config.path) { get { handleWebSocketMessages(newUser(port)) } @@ -103,17 +102,8 @@ class JsonRpcServer( } } - /** Binds this server instance to a given port and interface, allowing - * future connections. - * - * @param interface the interface to bind to. - * @param port the port to bind to. - * @return a server binding object. - */ - def bind(interface: String, port: Int): Future[Http.ServerBinding] = - Http() - .newServerAt(interface, port) - .bind(route(port)) + override protected def secureConfig(): Option[SecureConnectionConfig] = + config.secureConfig } object JsonRpcServer { @@ -129,6 +119,7 @@ object JsonRpcServer { case class Config( outgoingBufferSize: Int, lazyMessageTimeout: FiniteDuration, + secureConfig: Option[SecureConnectionConfig], path: String = "" ) @@ -139,7 +130,11 @@ object JsonRpcServer { * @return a default config. */ def default: Config = - Config(outgoingBufferSize = 1000, lazyMessageTimeout = 10.seconds) + Config( + outgoingBufferSize = 1000, + lazyMessageTimeout = 10.seconds, + secureConfig = None + ) } case class WebConnect(webActor: ActorRef, port: Int) diff --git a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/SecureConnectionConfig.scala b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/SecureConnectionConfig.scala new file mode 100644 index 0000000000..29283c091a --- /dev/null +++ b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/SecureConnectionConfig.scala @@ -0,0 +1,209 @@ +package org.enso.jsonrpc + +import com.typesafe.config.Config + +import java.io.{ByteArrayInputStream, File, FileInputStream, InputStream} +import java.security.cert.{CertificateFactory, X509Certificate} +import java.security.spec.PKCS8EncodedKeySpec +import java.security.{KeyFactory, KeyStore, SecureRandom} +import java.util.Base64 +import javax.net.ssl.{ + KeyManagerFactory, + SSLContext, + TrustManager, + X509TrustManager +} +import scala.util.Try + +/** Base class for generating custom {@link SSLContext} from configs. + * + * @param trustSelfSignedCerts true, if the SLLContext should trust all self-signed certificates + */ +abstract class SecureConnectionConfig(trustSelfSignedCerts: Boolean) { + def generateSSLContext(): Try[SSLContext] + + protected def trustManagers: Array[TrustManager] = { + if (trustSelfSignedCerts) { + Array[TrustManager](new X509TrustManager { + override def checkClientTrusted( + chain: Array[X509Certificate], + authType: String + ): Unit = {} + + override def checkServerTrusted( + chain: Array[X509Certificate], + authType: String + ): Unit = {} + + override def getAcceptedIssuers: Array[X509Certificate] = new Array(0) + }) + } else { + null + } + } +} + +object SecureConnectionConfig { + + /** Infers secure configuration from application's config. + * If the config has a `pkcs12-file` key, the configuration will be for the PKCS12-formatted certificate. + * If the config has a `public-certificate` key, the configuration will be inferred for the public/public certificate/key. + * If none of the above, returns a failure since no secure configuration is present in the application's config. + * + * @param config application.conf config + * @return left value with a failure or right value with a validated secure configuration + */ + def fromApplicationConfig( + config: Config + ): Either[Option[String], SecureConnectionConfig] = { + if (config.hasPath("pkcs12-file")) { + (for { + pkcs12 <- getStringFieldOpt(config, "pkcs12-file") + pkcs12File = new File(pkcs12) + password <- getStringFieldOpt(config, "pkcs12-password") + trustSelfSigned <- getBooleanFieldOpt(config, "trust-self-signed") + } yield SecureConnectionConfigForPKCS12(pkcs12File, password)( + trustSelfSigned + )).left.map(Some(_)) + } else if (config.hasPath("public-certificate")) { + (for { + publicKeyCertificate <- getStringFieldOpt(config, "public-certificate") + publicKeyCertificateAlg <- getStringFieldOpt( + config, + "public-certificate-algorithm" + ) + privateKey <- getStringFieldOpt(config, "private-key") + trustSelfSigned <- getBooleanFieldOpt(config, "trust-self-signed") + } yield SecureConnectionConfigForPublicPrivateCert( + publicKeyCertificate, + publicKeyCertificateAlg, + privateKey + )(trustSelfSigned)).left.map(Some(_)) + + } else Left(None) + } + + private def getStringFieldOpt( + config: Config, + fieldName: String + ): Either[String, String] = { + if (config.hasPath(fieldName)) { + val v = config.getString(fieldName) + if (v == null || v.isEmpty) Left(s"field $fieldName is empty") + else Right(v) + } else { + Left(s"missing $fieldName") + } + } + + private def getBooleanFieldOpt( + config: Config, + fieldName: String + ): Either[String, Boolean] = { + if (config.hasPath(fieldName)) { + Right(config.getBoolean(fieldName)) + } else { + Left(s"missing $fieldName") + } + } + + /** Configuration for SSLContext from PKCS12 format with a corresponding private password. + * Generation of the SSLContext will create a local Java keystore on-the-fly, based on the provided PKCS12 bundle. + * + * @param pkcsInputStream input stream to PKCS12-formatted object + * @param password password to a certificate + * @param trustSelfSignedCertificates true, if all self-signed certificates should be trusted + */ + case class SecureConnectionConfigForPKCS12( + pkcsInputStream: InputStream, + password: String + )(trustSelfSignedCertificates: Boolean) + extends SecureConnectionConfig(trustSelfSignedCertificates) { + + private val keystoreType = "PKCS12" + private val keyManagerFactoryAlgorithm = "SunX509" + private val sslType = "TLS" + + def generateSSLContext(): Try[SSLContext] = Try { + val keyStore = KeyStore.getInstance(keystoreType) + val passwordChars = password.toCharArray + keyStore.load(pkcsInputStream, passwordChars) + + val kmf = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm) + kmf.init(keyStore, passwordChars) + val keyManagers = kmf.getKeyManagers + val sslContext = SSLContext.getInstance(sslType) + sslContext.init(keyManagers, trustManagers, new SecureRandom()) + sslContext + } + } + + object SecureConnectionConfigForPKCS12 { + def apply( + pkcsFile: File, + password: String + )(trustSelfSignedCertificates: Boolean): SecureConnectionConfig = + SecureConnectionConfigForPKCS12( + new FileInputStream(pkcsFile), + password + )(trustSelfSignedCertificates) + } + + /** Configuration for SSLContext from certificate and a corresponding private key. + * Generation of the SSLContext will create a local Java keystore on-the-fly, based on the provided values. + * + * @param publicCertificate contents of the certificate + * @param publicCertificateAlgorithm algorithm used in the certificate + * @param privateKey private key + * @param trustSelfSignedCertificates true, if all self-signed certificates should be trusted + */ + case class SecureConnectionConfigForPublicPrivateCert( + publicCertificate: String, + publicCertificateAlgorithm: String, + privateKey: String + )(trustSelfSignedCertificates: Boolean) + extends SecureConnectionConfig(trustSelfSignedCertificates) { + + private val beginPrivateHeader = "-----BEGIN PRIVATE KEY-----" + private val endPrivateSuffix = "-----END PRIVATE KEY-----" + private val targetKeyStore = "PKCS12" + private val publicPrivateAlg = "RSA" + private val sslType = "TLS" + private val keyManagerFactoryAlgorithm = "SunX509" + + override def generateSSLContext(): Try[SSLContext] = Try { + val factory = CertificateFactory.getInstance(publicCertificateAlgorithm) + val certificateStream = + new ByteArrayInputStream(publicCertificate.getBytes()) + val cert = factory.generateCertificate(certificateStream) + + val prefixIdx = privateKey.indexOf(beginPrivateHeader) + val privateKeyWithDroppedAttributes = + if (prefixIdx == -1) privateKey else privateKey.substring(prefixIdx) + + val privateKeyPEM = privateKeyWithDroppedAttributes + .replaceAll("\\R", "") + .replace(beginPrivateHeader, "") + .replace(endPrivateSuffix, "") + val privateKeyDER = Base64.getDecoder().decode(privateKeyPEM); + + val spec = new PKCS8EncodedKeySpec(privateKeyDER); + val keyFactory = KeyFactory.getInstance(publicPrivateAlg); + val storePrivateKey = keyFactory.generatePrivate(spec); + val keyStore = KeyStore.getInstance(targetKeyStore) + keyStore.load(null) + val password = "temp-keystore".toCharArray + keyStore.setKeyEntry("enso", storePrivateKey, password, Array(cert)) + + val kmf = + KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm) + kmf.init(keyStore, password) + val keyManagers = kmf.getKeyManagers + val sslContext = SSLContext.getInstance(sslType) + + sslContext.init(keyManagers, trustManagers, new SecureRandom()) + sslContext + } + } + +} diff --git a/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/Server.scala b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/Server.scala new file mode 100644 index 0000000000..e15ab30469 --- /dev/null +++ b/lib/scala/json-rpc-server/src/main/scala/org/enso/jsonrpc/Server.scala @@ -0,0 +1,55 @@ +package org.enso.jsonrpc + +import akka.actor.ActorSystem +import akka.http.scaladsl.{ConnectionContext, Http} +import akka.http.scaladsl.server.Route + +import scala.concurrent.Future + +abstract class Server(implicit private val system: ActorSystem) { + + /** Binds this server instance to a given port and interface, allowing + * future connections. + * + * @param interface the interface to bind to + * @param port the port to bind to + * @param secure true if the port should refer to a secure binding + * @return a server binding object + */ + def bind( + interface: String, + port: Int, + secure: Boolean = false + ): Future[Http.ServerBinding] = { + val httpServer = Http() + .newServerAt(interface, port) + if (secure) { + val httpsContext = secureConfig().flatMap(config => { + config + .generateSSLContext() + .map(ctx => ConnectionContext.httpsServer(ctx)) + .toOption + + }) + httpsContext match { + case Some(ctx) => + httpServer.enableHttps(ctx).bind(serverRoute(port)) + case None => + Future.failed(new RuntimeException("HTTPS misconfigured")) + } + } else { + httpServer.bind(serverRoute(port)) + } + } + + /** Returns handlers for http requests supported by the server. + * + * @param port port number where the server will be listening to handle requests + * @return mapping between requests and responses supported by this server + */ + protected def serverRoute(port: Int): Route + + /** Returns an optional configuration for supporting secure connections. */ + protected def secureConfig(): Option[SecureConnectionConfig] + +} diff --git a/lib/scala/json-rpc-server/src/test/java/org/enso/jsonrpc/SSLContextBuilderTest.java b/lib/scala/json-rpc-server/src/test/java/org/enso/jsonrpc/SSLContextBuilderTest.java new file mode 100644 index 0000000000..b7ff24d926 --- /dev/null +++ b/lib/scala/json-rpc-server/src/test/java/org/enso/jsonrpc/SSLContextBuilderTest.java @@ -0,0 +1,100 @@ +package org.enso.jsonrpc; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.bootstrap.HttpServer; +import org.apache.http.impl.bootstrap.ServerBootstrap; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.enso.jsonrpc.SecureConnectionConfig.SecureConnectionConfigForPublicPrivateCert; +import org.junit.After; +import org.junit.Test; + +public class SSLContextBuilderTest { + // See https://lightbend.github.io/ssl-config/CertificateGeneration.html or + // https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore + // on how to generate test public/private certificates or pkcs12 files. + + HttpServer httpServer = null; + + @After + public void stop() { + if (httpServer != null) { + httpServer.shutdown(0, TimeUnit.SECONDS); + httpServer = null; + } + } + + private void testSSLContext(int port, SSLContext ctx) throws IOException { + var sslsf = new SSLConnectionSocketFactory(ctx, NoopHostnameVerifier.INSTANCE); + var socketFactoryRegistry = + RegistryBuilder.create() + .register("https", sslsf) + .register("http", new PlainConnectionSocketFactory()) + .build(); + + var connectionManager = new BasicHttpClientConnectionManager(socketFactoryRegistry); + var httpClient = + HttpClients.custom() + .setSSLSocketFactory(sslsf) + .setConnectionManager(connectionManager) + .build(); + httpServer = + ServerBootstrap.bootstrap() + .setLocalAddress(InetAddress.getByName("localhost")) + .setListenerPort(port) + .setSslContext(ctx) + .setSslSetupHandler(socket -> socket.setNeedClientAuth(true)) + .registerHandler( + "/*", (request, response, context) -> response.setStatusCode(HttpStatus.SC_OK)) + .create(); + httpServer.start(); + var request = new HttpGet("https://localhost:" + port); + var response = httpClient.execute(request); + assertEquals(response.getStatusLine().getStatusCode(), 200); + } + + @Test + public void testCreatingSSLContextFromPKCS12() throws IOException { + var pcksFile = this.getClass().getResourceAsStream("/example.com.p12"); + assertNotNull(pcksFile); + var password = "E4FtHvrLA4"; + var secureConnection = + new SecureConnectionConfig.SecureConnectionConfigForPKCS12(pcksFile, password, true); + var sslContext = secureConnection.generateSSLContext(); + assertTrue(sslContext.isSuccess()); + var ctx = sslContext.get(); + testSSLContext(8444, ctx); + } + + @Test + public void testCreatingSSLContextFromCertificate() throws IOException { + var certFile = this.getClass().getResourceAsStream("/example.com.crt"); + var privateKey = this.getClass().getResourceAsStream("/example.com.key"); + assertNotNull(certFile); + assertNotNull(privateKey); + var secureConnection = + new SecureConnectionConfigForPublicPrivateCert( + IOUtils.toString(certFile, StandardCharsets.UTF_8), + "X.509", + IOUtils.toString(privateKey, StandardCharsets.UTF_8), + true); + var sslContext = secureConnection.generateSSLContext(); + assertTrue(sslContext.isSuccess()); + var ctx = sslContext.get(); + testSSLContext(8443, ctx); + } +} diff --git a/lib/scala/json-rpc-server/src/test/resources/example.com.crt b/lib/scala/json-rpc-server/src/test/resources/example.com.crt new file mode 100644 index 0000000000..94eae7562b --- /dev/null +++ b/lib/scala/json-rpc-server/src/test/resources/example.com.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFATCCAumgAwIBAgIJAM8h7U1r8N5xMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNV +BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRgwFgYDVQQKEw9FeGFtcGxlIENvbXBhbnkxFDASBgNVBAsTC0V4YW1wbGUg +T3JnMRIwEAYDVQQDEwlleGFtcGxlQ0EwHhcNMjMwOTI3MTA1NTQ2WhcNMjMxMjI2 +MTA1NTQ2WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU +BgNVBAcTDVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0V4YW1wbGUgQ29tcGFueTEU +MBIGA1UECxMLRXhhbXBsZSBPcmcxFDASBgNVBAMTC2V4YW1wbGUuY29tMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApcZxK40JHLuZfPMlOIHqEkYIZj13 +UyeiDs8TLqd+FUVWAWWqkwcAqr2gcKZL584lWVHYMgaFD6eywoyCRKLmWE0qEjn+ +lD6A+UuDn4o3+6l5hJdF9OObPDGINrPLs444qNRVaGsyxtH5zwmJMM8tky5oCyva +n+YUfpvB3LB4FCflHcgc0cHcEz4syhzxTv+0iKJGH76GfLje+Y/iyI3NH0INxYG2 +009B0VDbyjOTTYtebj66FjxntpFVyu3ZIBnx/ygNlBmuSIQNwmM9hDUxB8CaI/fq +HQGrnJYe156iK42IQIHLTYc3vMGfvp2zxDJ8t/vDrqeD/EAZgIXte5GDPQIDAQAB +o38wfTAdBgNVHQ4EFgQUxgi/ASjTwUmVrTXPTfxzDBN9d58wDgYDVR0PAQH/BAQD +AgWgMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMB8GA1UdIwQYMBaAFMc9QhmtfnnL +LOtK7N/o4qFMTNDWMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBDAUA +A4ICAQAV//cGfHhnyh+AFYHrM8V/utHjs05b+l0yFl8l68xffVxnhPRVB1grd2zA +6AjKP5QxB2WtZjTBudCfk8j6nFvsgWtYed0r2Bx/VRG3j5acNkSgrUiwrdKNCxGq +QeHOckk3nWc/vFIuZCEjvO63XwZG3zCSmg8Ut6yZYFDAcftrzUHA7Eo5vVRS3irI +elSgpvoXmDBNe7lbd699aBhWcudT0fj7laD99A5S/f1W00WappP4KD1ifwBICDoe +aboQZNCcw46FWxKLHZ1nyyO8pp/UfKTvJMu2zn+E6Yn97QPJ7CnwtLTU1Lm90CHh +dSC0nB7zDiPNe8yvmX1zIAYO/UGx+8siLWqlm25kH8ufAmCcRIyytn04dGUv37Vp +sLRWhpebsx6xXTgyd/NYqSvXCjf7Gr3p74mP5w7XM9zpRUM825lA6gdR2cCm0QJ0 +7Aq2YbgI8pkdh9dM5vlX0CmjoPEBsQ3G9rcLW+LgalMD44qzFYmTHWuHKfas06AU +2dOAAMAWsPqELDBTP5O+Oafusf5VOrpLCYTk54xzfuQ+USpqSAFPaBhhkSa+6i3g +aPmOdmtCVM27qipflHOkJ1KHFH0Hj/QRV4jPXHc+ov75R23v/MhckdDjADL+J20S +yVLwXE5iO0Yyfqpg/qd31P2PgqZboLPCfvy7mPuCN0jMClsuOw== +-----END CERTIFICATE----- diff --git a/lib/scala/json-rpc-server/src/test/resources/example.com.key b/lib/scala/json-rpc-server/src/test/resources/example.com.key new file mode 100644 index 0000000000..0134ceb75a --- /dev/null +++ b/lib/scala/json-rpc-server/src/test/resources/example.com.key @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: example.com + localKeyID: 54 69 6D 65 20 31 36 39 35 38 31 32 34 30 30 35 31 30 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClxnErjQkcu5l8 +8yU4geoSRghmPXdTJ6IOzxMup34VRVYBZaqTBwCqvaBwpkvnziVZUdgyBoUPp7LC +jIJEouZYTSoSOf6UPoD5S4Ofijf7qXmEl0X045s8MYg2s8uzjjio1FVoazLG0fnP +CYkwzy2TLmgLK9qf5hR+m8HcsHgUJ+UdyBzRwdwTPizKHPFO/7SIokYfvoZ8uN75 +j+LIjc0fQg3FgbbTT0HRUNvKM5NNi15uProWPGe2kVXK7dkgGfH/KA2UGa5IhA3C +Yz2ENTEHwJoj9+odAauclh7XnqIrjYhAgctNhze8wZ++nbPEMny3+8Oup4P8QBmA +he17kYM9AgMBAAECggEABmRbcsFya4A0T+QUOFSSEPhQVJjkI/mwPv/vDmp46xsM +UOt5o0eu1+aN7CoNXTfOqt4EBxHHSa4+r0+5qinZ1efLyn4f+dlbIcGuppUuyW5k +eB9ZHDM2hiCmzu0p2peOSbw+OtN/Vrk796a3eoFSkY1Fh8C+IlI90g/xbrADQUuk +n+7+YYZyauhJnqKp/PvZza30gRQ2+Q+0KmDJluX9G4Y8xzRw5w9zFZA3Fsq20ND4 +mPPnGWfqmbcXAx9M750ZWcyqksArI+DR9yjCzD0uhbmcs5s0Jacxz2LJTpoYdnSA +CP/7IMmii+F2DUQj3b7ikoIqu5wYnSZDALTZpeCDhQKBgQDNC0VEGs12HH9akMqJ +CxZv3hxObsQ6HQaTJ9UgQa4iLHJsd2i21/x4tRGDr53V2VNgdzpazn8kBxyN3Dpv +swILed4L0ieFPzPpb3XUIcvpDYfiH2zLHbkJ/j1ctzF9hwtrtFtJZiIaDvULuAz5 +VZdUYmkbOuju31swrLBdQDMm4wKBgQDO+Otcw9aE04VTzU0TggcszjUbWPisFHV9 +WXv0GOXyuO4xnwTURKIX7M5SbhS3Umd2bOLvnCAjZ/cwdWfu2s3ZdmU4a6jTbZwj +HD/69gQZypgbEqJySP/ytKzFog6eQVVizZenNTjPuPEoD6nWtT3SlvEM+F8HQn3D +wLPK6minXwKBgGSQpY1Mk/7c5T13DE8AqCV/y2RQgV97QvFDtQ0YCZ7rK8e1HR+o +eUR+kjODG6d56qHCeFV3N/ZkooWVQPft1Q/p2pTzoryAjiZsq166oLcSEtY90W8h +idKz5kal9tj5NgnCMI+kTw92zIrN31ceupUBm0lmsD4QQDp0SB/EUBI/AoGBAMzV +lTmtp7S1EhKp8EoKOceiHPT/zLuhU9XGpeIichER7Mq3MjRR6In9FwSFZYM4zqRp +hv7UaQohboZK6518dpVtkyePhPoaVJh68OcSa2SLGJZSjurETGqLXSILDKSazEKI +bjpRdfQ+eIzJU7DmllTAhbfsZz/sEkOVh1qfOwvJAoGAWixb3IUGnzm+vt/MluNj +0GaNEsknkkuaLmpX93G8nsEySp89epLKsq/tclRgmCnYrkjt5jkDvO7H167uJ4Dn +gpFjfAT4EwhzDX2j7edLBZ/PiS67WDL3MajBLAd3JDHJFmbi449wJreQ1O9wPl84 +OHUQNH6HX4pYG8wGvgOTz/0= +-----END PRIVATE KEY----- diff --git a/lib/scala/json-rpc-server/src/test/resources/example.com.p12 b/lib/scala/json-rpc-server/src/test/resources/example.com.p12 new file mode 100644 index 0000000000000000000000000000000000000000..37d42e8f29dd746d0955d8a570db4ce58efe596c GIT binary patch literal 4850 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 literal 0 HcmV?d00001 diff --git a/lib/scala/project-manager/src/main/resources/application.conf b/lib/scala/project-manager/src/main/resources/application.conf index da488fb546..9e565f43b8 100644 --- a/lib/scala/project-manager/src/main/resources/application.conf +++ b/lib/scala/project-manager/src/main/resources/application.conf @@ -91,6 +91,9 @@ project-manager { max-port = 65535 max-port = ${?NETWORK_MAX_PORT} + + enable-secure = false + enable-secure = ${?NETWORK_ENABLE_HTTPS} } server { diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala index 647788d2ee..60b5ec0de8 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/boot/configuration.scala @@ -83,8 +83,14 @@ object configuration { * @param interface an interface to listen to * @param minPort min port for the LS * @param maxPort max port for the LS + * @param enableSecure true, if secure connections should be enabled, false otherwise */ - case class NetworkConfig(interface: String, minPort: Int, maxPort: Int) + case class NetworkConfig( + interface: String, + minPort: Int, + maxPort: Int, + enableSecure: Boolean + ) /** A configuration object for bootloader properties. * diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/data/LanguageServerSockets.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/data/LanguageServerSockets.scala index 11d27d9122..d71611c7bf 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/data/LanguageServerSockets.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/data/LanguageServerSockets.scala @@ -3,6 +3,13 @@ package org.enso.projectmanager.data /** Sockets that a language server listens on. * * @param jsonSocket a socket used for JSON-RPC protocol + * @param secureJsonSocket a secure socket used for JSON-RPC protocol * @param binarySocket a socket used for the binary protocol + * @param secureBinarySocket a secure socket used for the binary protocol */ -case class LanguageServerSockets(jsonSocket: Socket, binarySocket: Socket) +case class LanguageServerSockets( + jsonSocket: Socket, + secureJsonSocket: Option[Socket], + binarySocket: Socket, + secureBinarySocket: Option[Socket] +) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnection.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnection.scala index 4520be87e9..0f8fc6e7d4 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnection.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnection.scala @@ -2,11 +2,14 @@ package org.enso.projectmanager.infrastructure.http import akka.NotUsed import akka.actor.{ActorRef, ActorSystem, Props} -import akka.http.scaladsl.Http +import akka.http.scaladsl.{ConnectionContext, Http} import akka.http.scaladsl.model.ws._ import akka.pattern.pipe import akka.stream.scaladsl.{Flow, Sink, Source} import akka.stream.{CompletionStrategy, OverflowStrategy} +import com.typesafe.scalalogging.Logger +import org.enso.jsonrpc.SecureConnectionConfig +import org.enso.projectmanager.infrastructure.http import org.enso.projectmanager.infrastructure.http.AkkaBasedWebSocketConnection._ import org.enso.projectmanager.infrastructure.http.FanOutReceiver.{ Attach, @@ -24,10 +27,15 @@ import org.enso.projectmanager.infrastructure.http.WebSocketConnection.{ * @param address a server address * @param system an actor system */ -class AkkaBasedWebSocketConnection(address: String)(implicit +class AkkaBasedWebSocketConnection( + address: String, + secureConfig: Option[SecureConnectionConfig] +)(implicit system: ActorSystem ) extends WebSocketConnection { + private lazy val logger = Logger[http.AkkaBasedWebSocketConnection.type] + import system.dispatcher private val receiver = system.actorOf(Props(new FanOutReceiver)) @@ -76,8 +84,25 @@ class AkkaBasedWebSocketConnection(address: String)(implicit /** @inheritdoc */ def connect(): Unit = { + val server = Http() + secureConfig + .flatMap { config => + { + val ctx = config + .generateSSLContext() + .map(sslContext => ConnectionContext.httpsClient(sslContext)) + if (ctx.isFailure) { + logger.warn( + "failed to establish requested secure context: {}", + ctx.failed.get.getMessage + ) + } + ctx.toOption + } + } + .foreach(ctx => server.setDefaultClientHttpsContext(ctx)) val (future, _) = - Http() + server .singleWebSocketRequest( WebSocketRequest(address), flow diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnectionFactory.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnectionFactory.scala index 3c7e759f7a..eea4176d14 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnectionFactory.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/AkkaBasedWebSocketConnectionFactory.scala @@ -1,5 +1,6 @@ package org.enso.projectmanager.infrastructure.http import akka.actor.ActorSystem +import org.enso.jsonrpc.SecureConnectionConfig import org.enso.projectmanager.data.Socket /** A factory of Akka-based web socket connections. @@ -9,6 +10,19 @@ class AkkaBasedWebSocketConnectionFactory(implicit system: ActorSystem) /** @inheritdoc */ override def createConnection(socket: Socket): WebSocketConnection = - new AkkaBasedWebSocketConnection(s"ws://${socket.host}:${socket.port}") + new AkkaBasedWebSocketConnection( + s"ws://${socket.host}:${socket.port}", + None + ) + /** @inheritdoc */ + override def createSecureConnection( + socket: Socket, + secureConfig: SecureConnectionConfig + ): WebSocketConnection = { + new AkkaBasedWebSocketConnection( + s"wss://${socket.host}:${socket.port}", + Some(secureConfig) + ) + } } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/WebSocketConnectionFactory.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/WebSocketConnectionFactory.scala index 74bd2dc177..ef766ec9c2 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/WebSocketConnectionFactory.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/http/WebSocketConnectionFactory.scala @@ -1,5 +1,6 @@ package org.enso.projectmanager.infrastructure.http +import org.enso.jsonrpc.SecureConnectionConfig import org.enso.projectmanager.data.Socket /** Abstract connection factory. @@ -13,4 +14,14 @@ trait WebSocketConnectionFactory { */ def createConnection(socket: Socket): WebSocketConnection + /** Creates a secure web socket connection. + * + * @param socket a server address + * @return a secure connection + */ + def createSecureConnection( + socket: Socket, + secureConfig: SecureConnectionConfig + ): WebSocketConnection + } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala index 708b70cbe3..fa7f5a4f89 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ExecutorWithUnlimitedPool.scala @@ -36,7 +36,9 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor { descriptor: LanguageServerDescriptor, progressTracker: ActorRef, rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], lifecycleListener: LanguageServerExecutor.LifecycleListener ): Unit = { val runnable: Runnable = { () => @@ -45,7 +47,9 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor { descriptor, progressTracker, rpcPort, + secureRpcPort, dataPort, + secureDataPort, lifecycleListener ) } catch { @@ -66,7 +70,9 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor { descriptor: LanguageServerDescriptor, progressTracker: ActorRef, rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], lifecycleListener: LanguageServerExecutor.LifecycleListener ): Unit = { val distributionConfiguration = descriptor.distributionConfiguration @@ -76,10 +82,12 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor { val inheritedLogLevel = LoggingServiceManager.currentLogLevelForThisApplication() val options = LanguageServerOptions( - rootId = descriptor.rootId, - interface = descriptor.networkConfig.interface, - rpcPort = rpcPort, - dataPort = dataPort + rootId = descriptor.rootId, + interface = descriptor.networkConfig.interface, + rpcPort = rpcPort, + secureRpcPort = secureRpcPort, + dataPort = dataPort, + secureDataPort = secureDataPort ) val configurationManager = new GlobalRunnerConfigurationManager( versionManager, diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerBootLoader.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerBootLoader.scala index 6c1e55fc7d..9c060610c5 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerBootLoader.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerBootLoader.scala @@ -74,20 +74,32 @@ class LanguageServerBootLoader( while (binaryPort == jsonRpcPort) { binaryPort = findPort() } + var secureJsonRpcPort: Option[Int] = None + var secureBinaryPort: Option[Int] = None + if (descriptor.networkConfig.enableSecure) { + val regularPorts = Set(jsonRpcPort, binaryPort) + secureJsonRpcPort = Some(findPort(regularPorts)) + secureBinaryPort = + Some(findPort(regularPorts + secureJsonRpcPort.get)) + } logger.info( "Found sockets for the language server " + - "[json:{}:{}, binary:{}:{}].", + "[json:{}:{}:{}, binary:{}:{}:{}].", descriptor.networkConfig.interface, jsonRpcPort, + secureJsonRpcPort.getOrElse("none"), descriptor.networkConfig.interface, - binaryPort + binaryPort, + secureBinaryPort.getOrElse("none") ) self ! Boot context.become( bootingFirstTime( - rpcPort = jsonRpcPort, - dataPort = binaryPort, - retryCount = retry + rpcPort = jsonRpcPort, + secureRpcPort = secureJsonRpcPort, + dataPort = binaryPort, + secureDataPort = secureBinaryPort, + retryCount = retry ) ) @@ -102,16 +114,20 @@ class LanguageServerBootLoader( */ private def bootingFirstTime( rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], retryCount: Int ): Receive = LoggingReceive.withLabel("bootingFirstTime") { booting( - rpcPort = rpcPort, - dataPort = dataPort, - shouldRetry = true, - retryCount = retryCount, - bootRequester = context.parent + rpcPort = rpcPort, + secureRpcPort = secureRpcPort, + dataPort = dataPort, + secureDataPort = secureDataPort, + shouldRetry = true, + retryCount = retryCount, + bootRequester = context.parent ) } @@ -123,7 +139,9 @@ class LanguageServerBootLoader( */ private def booting( rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], shouldRetry: Boolean, retryCount: Int, bootRequester: ActorRef @@ -136,7 +154,9 @@ class LanguageServerBootLoader( descriptor = descriptor, bootTimeout = bootTimeout, rpcPort = rpcPort, + secureRpcPort = secureRpcPort, dataPort = dataPort, + secureDataPort = secureDataPort, executor = executor ), s"process-wrapper-${descriptor.name}" @@ -164,8 +184,10 @@ class LanguageServerBootLoader( case LanguageServerProcess.ServerConfirmedFinishedBooting => val connectionInfo = LanguageServerConnectionInfo( descriptor.networkConfig.interface, - rpcPort = rpcPort, - dataPort = dataPort + rpcPort = rpcPort, + secureRpcPort = secureRpcPort, + dataPort = dataPort, + secureDataPort = secureDataPort ) logger.info("Language server booted [{}].", connectionInfo) @@ -241,11 +263,13 @@ class LanguageServerBootLoader( ): Receive = LoggingReceive.withLabel("rebooting") { booting( - rpcPort = connectionInfo.rpcPort, - dataPort = connectionInfo.dataPort, - shouldRetry = false, - retryCount = config.numberOfRetries, - bootRequester = rebootRequester + rpcPort = connectionInfo.rpcPort, + secureRpcPort = connectionInfo.secureRpcPort, + dataPort = connectionInfo.dataPort, + secureDataPort = connectionInfo.secureDataPort, + shouldRetry = false, + retryCount = config.numberOfRetries, + bootRequester = rebootRequester ) } @@ -281,11 +305,12 @@ class LanguageServerBootLoader( } } - private def findPort(): Int = + private def findPort(excludePorts: Set[Int] = Set.empty): Int = Tcp.findAvailablePort( descriptor.networkConfig.interface, descriptor.networkConfig.minPort, - descriptor.networkConfig.maxPort + descriptor.networkConfig.maxPort, + excludePorts ) private case object FindFreeSocket diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerConnectionInfo.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerConnectionInfo.scala index 44dbd50c13..69b4aea8fb 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerConnectionInfo.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerConnectionInfo.scala @@ -6,5 +6,7 @@ package org.enso.projectmanager.infrastructure.languageserver case class LanguageServerConnectionInfo( interface: String, rpcPort: Int, - dataPort: Int + secureRpcPort: Option[Int], + dataPort: Int, + secureDataPort: Option[Int] ) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala index 3314c8cf15..6b40f3984f 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala @@ -195,7 +195,13 @@ class LanguageServerController( sender() ! ServerStarted( LanguageServerSockets( Socket(connectionInfo.interface, connectionInfo.rpcPort), - Socket(connectionInfo.interface, connectionInfo.dataPort) + connectionInfo.secureRpcPort.map(port => + Socket(connectionInfo.interface, port) + ), + Socket(connectionInfo.interface, connectionInfo.dataPort), + connectionInfo.secureDataPort.map(port => + Socket(connectionInfo.interface, port) + ) ) ) context.become( diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerExecutor.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerExecutor.scala index 6a93986218..7fe9dc19a5 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerExecutor.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerExecutor.scala @@ -12,7 +12,8 @@ trait LanguageServerExecutor { * @param progressTracker reference to an actor that should be notifed of any * locks * @param rpcPort port to use for the RPC channel - * @param dataPort port to use for the binary channel + * @param secureRpcPort port to use for the RPC channel + * @param secureDataPort port to use for the binary channel * @param lifecycleListener a listener that will be notified when the process * is started and terminated */ @@ -20,7 +21,9 @@ trait LanguageServerExecutor { descriptor: LanguageServerDescriptor, progressTracker: ActorRef, rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], lifecycleListener: LifecycleListener ): Unit } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerProcess.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerProcess.scala index 807568b6b9..94bb510558 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerProcess.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerProcess.scala @@ -27,7 +27,9 @@ import scala.concurrent.duration.{DurationInt, FiniteDuration} * related to initializing the engine * @param descriptor a LS descriptor * @param rpcPort port to bind for RPC connections + * @param secureRpcPort an optional port to bind for secure RPC connections * @param dataPort port to bind for binary connections + * @param secureDataPort an optional port to bind for secure binary connections * @param bootTimeout maximum time permitted to wait for the process to finish * initializing; if the initialization heartbeat is not * received within this time the boot is treated as failed @@ -39,7 +41,9 @@ class LanguageServerProcess( progressTracker: ActorRef, descriptor: LanguageServerDescriptor, rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], bootTimeout: FiniteDuration, executor: LanguageServerExecutor ) extends Actor @@ -74,7 +78,9 @@ class LanguageServerProcess( descriptor = descriptor, progressTracker = progressTracker, rpcPort = rpcPort, + secureRpcPort = secureRpcPort, dataPort = dataPort, + secureDataPort = secureDataPort, lifecycleListener = LifecycleListener ) context.become(startingStage) @@ -199,7 +205,9 @@ object LanguageServerProcess { progressTracker: ActorRef, descriptor: LanguageServerDescriptor, rpcPort: Int, + secureRpcPort: Option[Int], dataPort: Int, + secureDataPort: Option[Int], bootTimeout: FiniteDuration, executor: LanguageServerExecutor ): Props = Props( @@ -207,7 +215,9 @@ object LanguageServerProcess { progressTracker, descriptor, rpcPort, + secureRpcPort, dataPort, + secureDataPort, bootTimeout, executor ) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/net/Tcp.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/net/Tcp.scala index 7211b1c31e..7dfefcb0a3 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/net/Tcp.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/net/Tcp.scala @@ -15,16 +15,22 @@ object Tcp { * @param host a host * @param minPort a minimum value of port * @param maxPort a maximum value of port + * @param excludeSet a set of ports that should never be selected * @return a port that is available to bind */ @tailrec - def findAvailablePort(host: String, minPort: Int, maxPort: Int): Int = { + def findAvailablePort( + host: String, + minPort: Int, + maxPort: Int, + excludeSet: Set[Int] = Set.empty + ): Int = { val random = Random.nextInt(maxPort - minPort + 1) val port = minPort + random - if (isPortAvailable(host, port)) { + if (!excludeSet.contains(port) && isPortAvailable(host, port)) { port } else { - findAvailablePort(host, minPort, maxPort) + findAvailablePort(host, minPort, maxPort, excludeSet + port) } } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala index 13698b78cd..fab3801c85 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala @@ -82,7 +82,9 @@ object ProjectManagementApi { case class Result( engineVersion: SemVer, languageServerJsonAddress: Socket, + languageServerSecureJsonAddress: Option[Socket], languageServerBinaryAddress: Socket, + languageServerSecureBinaryAddress: Option[Socket], projectName: String, projectNormalizedName: String, projectNamespace: String diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectOpenHandler.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectOpenHandler.scala index 263a34653c..81c2e00743 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectOpenHandler.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectOpenHandler.scala @@ -55,12 +55,14 @@ class ProjectOpenHandler[F[+_, +_]: Exec: CovariantFlatMap]( missingComponentAction = missingComponentAction ) } yield ProjectOpen.Result( - engineVersion = server.engineVersion, - languageServerJsonAddress = server.sockets.jsonSocket, - languageServerBinaryAddress = server.sockets.binarySocket, - projectName = server.projectName, - projectNormalizedName = server.projectNormalizedName, - projectNamespace = server.projectNamespace + engineVersion = server.engineVersion, + languageServerJsonAddress = server.sockets.jsonSocket, + languageServerSecureJsonAddress = server.sockets.secureJsonSocket, + languageServerBinaryAddress = server.sockets.binarySocket, + languageServerSecureBinaryAddress = server.sockets.secureBinarySocket, + projectName = server.projectName, + projectNormalizedName = server.projectNormalizedName, + projectNamespace = server.projectNamespace ) } diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala index 7d28549e63..6369750444 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala @@ -96,7 +96,9 @@ trait ProjectManagementOps { this: BaseServerSpec => ProjectOpen.Result( engineVer, jsonSock, + None, binSock, + None, projectName, normalizedName, namespace diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala index 42e235881e..95b972e25b 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerSupervisorSpec.scala @@ -136,7 +136,9 @@ class LanguageServerSupervisorSpec LanguageServerConnectionInfo( testHost, testRpcPort, - testDataPort + secureRpcPort = None, + testDataPort, + secureDataPort = None ) val supervisionConfig = diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/LanguageServerOptions.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/LanguageServerOptions.scala index acacf4fd11..faab98d51d 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/LanguageServerOptions.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/LanguageServerOptions.scala @@ -7,11 +7,15 @@ import java.util.UUID * @param rootId an id of content root * @param interface a interface that the server listen to * @param rpcPort an RPC port that the server listen to + * @param secureRpcPort an option secure RPC port that the server listen to * @param dataPort a data port that the server listen to + * @param secureDataPort an optional secure data port that the server listen to */ case class LanguageServerOptions( rootId: UUID, interface: String, rpcPort: Int, - dataPort: Int + secureRpcPort: Option[Int], + dataPort: Int, + secureDataPort: Option[Int] ) diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala index 7c78b0d23a..5916d89ebb 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala @@ -127,8 +127,13 @@ class Runner( options.dataPort.toString, "--log-level", logLevel.name - ) ++ - Option.unless(logMasking)("--no-log-masking") + ) ++ options.secureRpcPort + .map(port => Seq("--secure-rpc-port", port.toString)) + .getOrElse(Seq.empty) ++ + options.secureDataPort + .map(port => Seq("--secure-data-port", port.toString)) + .getOrElse(Seq.empty) ++ + Option.unless(logMasking)(Seq("--no-log-masking")).getOrElse(Seq.empty) RunSettings( version, arguments ++ additionalArguments,