mirror of
https://github.com/enso-org/enso.git
synced 2024-11-26 08:52:58 +03:00
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 <bushevdv@gmail.com> * PR comment * typo * lint * make windows line endings happy --------- Co-authored-by: Dmitry Bushev <bushevdv@gmail.com>
This commit is contained in:
parent
bf76be6e6b
commit
cfba3c6887
@ -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)
|
||||
|
12
build.sbt
12
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
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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 <uuid>`: 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 <port>`: Data port for visualization protocol. Default value
|
||||
is 8081.
|
||||
- `--secure-rpc-port <port>`: (optional) Secure RPC port for processing all
|
||||
incoming connections.
|
||||
- `--secure-data-port <port>`: (optional) Secure data port for visualization
|
||||
protocol.
|
||||
|
||||
To run the Language Server on 127.0.0.1:8080 type:
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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]
|
||||
|
||||
}
|
@ -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.<ConnectionSocketFactory>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);
|
||||
}
|
||||
}
|
29
lib/scala/json-rpc-server/src/test/resources/example.com.crt
Normal file
29
lib/scala/json-rpc-server/src/test/resources/example.com.crt
Normal file
@ -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-----
|
32
lib/scala/json-rpc-server/src/test/resources/example.com.key
Normal file
32
lib/scala/json-rpc-server/src/test/resources/example.com.key
Normal file
@ -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: <No 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-----
|
BIN
lib/scala/json-rpc-server/src/test/resources/example.com.p12
Normal file
BIN
lib/scala/json-rpc-server/src/test/resources/example.com.p12
Normal file
Binary file not shown.
@ -91,6 +91,9 @@ project-manager {
|
||||
|
||||
max-port = 65535
|
||||
max-port = ${?NETWORK_MAX_PORT}
|
||||
|
||||
enable-secure = false
|
||||
enable-secure = ${?NETWORK_ENABLE_HTTPS}
|
||||
}
|
||||
|
||||
server {
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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]
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,9 @@ trait ProjectManagementOps { this: BaseServerSpec =>
|
||||
ProjectOpen.Result(
|
||||
engineVer,
|
||||
jsonSock,
|
||||
None,
|
||||
binSock,
|
||||
None,
|
||||
projectName,
|
||||
normalizedName,
|
||||
namespace
|
||||
|
@ -136,7 +136,9 @@ class LanguageServerSupervisorSpec
|
||||
LanguageServerConnectionInfo(
|
||||
testHost,
|
||||
testRpcPort,
|
||||
testDataPort
|
||||
secureRpcPort = None,
|
||||
testDataPort,
|
||||
secureDataPort = None
|
||||
)
|
||||
|
||||
val supervisionConfig =
|
||||
|
@ -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]
|
||||
)
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user