netty.netty.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.netty
import akka.stream.scaladsl.Sink
import akka.util.ByteString
import io.netty.buffer.{ByteBuf, Unpooled}
import io.netty.channel.{Channel, ChannelHandler, ChannelHandlerContext, ChannelPipeline, EventLoopGroup}
import io.netty.handler.codec.http._
import io.netty.handler.codec.http.websocketx._
import io.netty.handler.logging.LogLevel
import io.netty.handler.ssl._
import org.reactivestreams.{Processor, Publisher}
import otoroshi.env.Env
import otoroshi.next.extensions.{HttpListener, HttpListenerNames}
import otoroshi.next.proxy.ProxyEngine
import otoroshi.script.RequestHandler
import otoroshi.ssl.DynamicSSLEngineProvider
import otoroshi.utils.reactive.ReactiveStreamUtils
import otoroshi.utils.syntax.implicits._
import play.api.Logger
import play.api.http.websocket.Message
import play.api.http.{HttpChunk, HttpEntity, HttpRequestHandler}
import play.api.libs.crypto.CookieSignerProvider
import play.api.libs.json.Json
import play.api.mvc._
import play.core.server.common.WebSocketFlowHandler
import play.core.server.common.WebSocketFlowHandler.{MessageType, RawMessage}
import reactor.netty.{DisposableServer, NettyOutbound}
import reactor.netty.http.server.logging.{AccessLog, AccessLogArgProvider, AccessLogFactory}
import java.net.{InetSocketAddress, SocketAddress}
import java.security.{Provider, SecureRandom}
import java.util.function.{BiFunction, Function}
import javax.net.ssl._
import scala.concurrent.Await
import scala.concurrent.duration.{DurationInt, DurationLong}
import scala.util.{Failure, Success, Try}
import reactor.netty.resources.LoopResources
import java.util.concurrent.atomic.AtomicReference
case class HttpServerBodyResponse(
body: Publisher[Array[Byte]],
contentType: Option[String],
contentLength: Option[Long],
chunked: Boolean
)
object ReactorNettyServer {
def classic(env: Env): ReactorNettyServer = {
new ReactorNettyServer(ReactorNettyServerConfig.parseFromWithCache(env), env)
}
}
class ReactorNettyServer(config: ReactorNettyServerConfig, env: Env) {
import reactor.core.publisher.Flux
import reactor.netty.http.HttpProtocol
import reactor.netty.http.server._
implicit private val ec = env.otoroshiExecutionContext
implicit private val mat = env.otoroshiMaterializer
implicit private val ev = env
private val logger = Logger("otoroshi-experimental-netty-server")
private val engine: ProxyEngine = env.scriptManager
.getAnyScript[RequestHandler](s"cp:${classOf[ProxyEngine].getName}")
.right
.get
.asInstanceOf[ProxyEngine]
private val cookieSignerProvider = new CookieSignerProvider(env.httpConfiguration.secret)
private val sessionCookieBaker =
new DefaultSessionCookieBaker(env.httpConfiguration.session, env.httpConfiguration.secret, cookieSignerProvider.get)
private val flashCookieBaker =
new DefaultFlashCookieBaker(env.httpConfiguration.flash, env.httpConfiguration.secret, cookieSignerProvider.get)
private val cookieEncoder = new DefaultCookieHeaderEncoding(env.httpConfiguration.cookies)
private def sendResultAsHttpResponse(result: Result, res: HttpServerResponse): NettyOutbound = {
val bresponse: HttpServerBodyResponse = result.body match {
case HttpEntity.NoEntity => HttpServerBodyResponse(Flux.empty[Array[Byte]](), None, None, false)
case HttpEntity.Strict(data, contentType) =>
HttpServerBodyResponse(Flux.just(Seq(data.toArray[Byte]): _*), contentType, Some(data.size.toLong), false)
case HttpEntity.Chunked(chunks, contentType) => {
val publisher = chunks
.collect { case HttpChunk.Chunk(data) =>
data.toArray[Byte]
}
.runWith(Sink.asPublisher(false))
HttpServerBodyResponse(publisher, contentType, None, true)
}
case HttpEntity.Streamed(data, contentLength, contentType) => {
val publisher = data.map(_.toArray[Byte]).runWith(Sink.asPublisher(false))
HttpServerBodyResponse(publisher, contentType, contentLength, false)
}
}
val headers = new DefaultHttpHeaders()
result.header.headers.map { case (key, value) =>
if (key != "otoroshi-netty-trailers") headers.add(key, value)
}
if (bresponse.contentType.contains("application/grpc")) {
headers.add(HttpHeaderNames.TRAILER, "grpc-status, grpc-message")
}
bresponse.contentType.foreach(ct => headers.add("Content-Type", ct))
bresponse.contentLength.foreach(cl => headers.addInt("Content-Length", cl.toInt))
res
.status(result.header.status)
.headers(headers)
.applyOnIf(result.newCookies.nonEmpty) { r =>
result.newCookies.map { cookie =>
val nettyCookie = new io.netty.handler.codec.http.cookie.DefaultCookie(cookie.name, cookie.value)
nettyCookie.setPath(cookie.path)
nettyCookie.setHttpOnly(cookie.httpOnly)
nettyCookie.setSecure(cookie.secure)
cookie.domain.foreach(d => nettyCookie.setDomain(d))
cookie.maxAge.foreach(d => nettyCookie.setMaxAge(d.toLong))
cookie.sameSite.foreach {
case play.api.mvc.Cookie.SameSite.None =>
nettyCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.None)
case play.api.mvc.Cookie.SameSite.Strict =>
nettyCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.Strict)
case play.api.mvc.Cookie.SameSite.Lax =>
nettyCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.Lax)
}
r.addCookie(nettyCookie)
}
r
}
.applyOnIf(result.newSession.isDefined) { r =>
result.newSession.map { session =>
val cookie = sessionCookieBaker.encodeAsCookie(session)
val sessionCookie = new io.netty.handler.codec.http.cookie.DefaultCookie(cookie.name, cookie.value)
sessionCookie.setPath(cookie.path)
sessionCookie.setHttpOnly(cookie.httpOnly)
sessionCookie.setSecure(cookie.secure)
cookie.domain.foreach(d => sessionCookie.setDomain(d))
cookie.maxAge.foreach(d => sessionCookie.setMaxAge(d.toLong))
cookie.sameSite.foreach {
case play.api.mvc.Cookie.SameSite.None =>
sessionCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.None)
case play.api.mvc.Cookie.SameSite.Strict =>
sessionCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.Strict)
case play.api.mvc.Cookie.SameSite.Lax =>
sessionCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.Lax)
}
r.addCookie(sessionCookie)
}
r
}
.applyOnIf(result.newFlash.isDefined) { r =>
result.newFlash.map { flash =>
val cookie = flashCookieBaker.encodeAsCookie(flash)
val flashCookie = new io.netty.handler.codec.http.cookie.DefaultCookie(cookie.name, cookie.value)
flashCookie.setPath(cookie.path)
flashCookie.setHttpOnly(cookie.httpOnly)
flashCookie.setSecure(cookie.secure)
cookie.domain.foreach(d => flashCookie.setDomain(d))
cookie.maxAge.foreach(d => flashCookie.setMaxAge(d.toLong))
cookie.sameSite.foreach {
case play.api.mvc.Cookie.SameSite.None =>
flashCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.None)
case play.api.mvc.Cookie.SameSite.Strict =>
flashCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.Strict)
case play.api.mvc.Cookie.SameSite.Lax =>
flashCookie.setSameSite(io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite.Lax)
}
r.addCookie(flashCookie)
}
r
}
.chunkedTransfer(bresponse.chunked)
.trailerHeaders(theaders => {
import collection.JavaConverters._
result.header.headers.get("otoroshi-netty-trailers").foreach { trailersId =>
otoroshi.netty.NettyRequestAwaitingTrailers.get(trailersId).foreach {
case Left(future) =>
// if (logger.isWarnEnabled) logger.warn(s"Unable to get trailer header for request '${trailersId}'")
otoroshi.netty.NettyRequestAwaitingTrailers.remove(trailersId)
val start = System.nanoTime()
val trailers = Await.result(future, 10.seconds)
if (logger.isWarnEnabled)
logger.warn(
s"Blocked thread for ${(System.nanoTime() - start).nanos.toHumanReadable} to get trailer header for request '${trailersId}'"
)
otoroshi.netty.NettyRequestAwaitingTrailers.remove(trailersId)
trailers.foreach { case (name, values) =>
try {
theaders.add(name, values.asJava)
} catch {
case e: Throwable => if (logger.isErrorEnabled) logger.error("error while adding trailer header", e)
}
}
case Right(trailers) =>
otoroshi.netty.NettyRequestAwaitingTrailers.remove(trailersId)
trailers.foreach { case (name, values) =>
try {
theaders.add(name, values.asJava)
} catch {
case e: Throwable => if (logger.isErrorEnabled) logger.error("error while adding trailer header", e)
}
}
}
}
})
.keepAlive(true)
.sendByteArray(bresponse.body)
}
private def frameToRawMessage(frame: WebSocketFrame): RawMessage = {
Try {
val builder = ByteString.newBuilder
frame.content().readBytes(builder.asOutputStream, frame.content().readableBytes())
val bytes = builder.result()
val messageType = frame match {
case _: TextWebSocketFrame => MessageType.Text
case _: BinaryWebSocketFrame => MessageType.Binary
case close: CloseWebSocketFrame => MessageType.Close
case _: PingWebSocketFrame => MessageType.Ping
case _: PongWebSocketFrame => MessageType.Pong
case _: ContinuationWebSocketFrame => MessageType.Continuation
}
RawMessage(messageType, bytes, frame.isFinalFragment)
} match {
case Failure(ex) =>
ex.printStackTrace()
RawMessage(MessageType.Text, ByteString("frameToRawMessage error: " + ex.getMessage), frame.isFinalFragment)
case Success(s) => s
}
}
private def messageToFrame(message: Message): WebSocketFrame = {
import io.netty.handler.codec.http.websocketx._
def byteStringToByteBuf(bytes: ByteString): ByteBuf = {
if (bytes.isEmpty) {
Unpooled.EMPTY_BUFFER
} else {
Unpooled.wrappedBuffer(bytes.asByteBuffer)
}
}
message match {
case play.api.http.websocket.TextMessage(data) => new TextWebSocketFrame(data)
case play.api.http.websocket.BinaryMessage(data) => new BinaryWebSocketFrame(byteStringToByteBuf(data))
case play.api.http.websocket.PingMessage(data) => new PingWebSocketFrame(byteStringToByteBuf(data))
case play.api.http.websocket.PongMessage(data) => new PongWebSocketFrame(byteStringToByteBuf(data))
case play.api.http.websocket.CloseMessage(Some(statusCode), reason) => new CloseWebSocketFrame(statusCode, reason)
case play.api.http.websocket.CloseMessage(None, _) => new CloseWebSocketFrame()
}
}
private def handleWebsocket(
listenerId: String,
req: HttpServerRequest,
res: HttpServerResponse,
version: String,
secure: Boolean,
session: Option[SSLSession]
): Publisher[Void] = {
ReactiveStreamUtils.FluxUtils.fromFPublisher[Void] {
val otoReq =
new ReactorNettyRequestHeader(listenerId, req, version, secure, session, sessionCookieBaker, flashCookieBaker)
engine.handleWsWithListener(otoReq, engine.badDefaultRoutingWs, config.exclusive).map {
case Left(result) => sendResultAsHttpResponse(result, res)
case Right(flow) => {
res.sendWebsocket { (wsInbound, wsOutbound) =>
val processor: Processor[RawMessage, Message] =
WebSocketFlowHandler.webSocketProtocol(65536).join(flow).toProcessor.run()
wsInbound
.receiveFrames()
.map[RawMessage](frameToRawMessage)
.subscribe(processor)
val fluxOut: Flux[WebSocketFrame] = Flux.from(processor).map(messageToFrame)
wsOutbound.sendObject(fluxOut)
}
}
}
}
}
private def handleHttp(
listenerId: String,
req: HttpServerRequest,
res: HttpServerResponse,
secure: Boolean,
channel: Channel
): Publisher[Void] = {
val parent = channel.parent()
val sslHandler =
Option(parent.pipeline().get(classOf[OtoroshiSslHandler]))
.orElse(Option(channel.pipeline().get(classOf[OtoroshiSslHandler])))
val version =
sslHandler.map { h => if (h.useH2) "HTTP/2.0" else req.version().toString }.getOrElse(req.version().toString)
val sessionOpt = sslHandler.map(_.engine.getSession)
sessionOpt.foreach(s => req.requestHeaders().set("Otoroshi-Tls-Version", TlsVersion.parse(s.getProtocol).name))
val isWebSocket = (req.requestHeaders().contains("Upgrade") || req.requestHeaders().contains("upgrade")) &&
(req.requestHeaders().contains("Sec-WebSocket-Version") || req
.requestHeaders()
.contains("Sec-WebSocket-Version".toLowerCase)) &&
Option(req.requestHeaders().get("Upgrade")).contains("websocket")
if (isWebSocket) {
handleWebsocket(listenerId, req, res, version, secure, sessionOpt)
} else {
ReactiveStreamUtils.FluxUtils.fromFPublisher[Void] {
val otoReq =
new ReactorNettyRequest(listenerId, req, version, secure, sessionOpt, sessionCookieBaker, flashCookieBaker)
engine.handleWithListener(otoReq, engine.badDefaultRoutingHttp, config.exclusive).map { result =>
sendResultAsHttpResponse(result, res)
}
}
}
}
private def handle(
listenerId: String,
req: HttpServerRequest,
res: HttpServerResponse,
secure: Boolean,
channel: Channel,
handler: HttpRequestHandler
): Publisher[Void] = {
val parent = channel.parent()
val sslHandler =
Option(parent.pipeline().get(classOf[OtoroshiSslHandler]))
.orElse(Option(channel.pipeline().get(classOf[OtoroshiSslHandler])))
val sessionOpt = sslHandler.map(_.engine.getSession)
sessionOpt.foreach(s => req.requestHeaders().set("Otoroshi-Tls-Version", TlsVersion.parse(s.getProtocol).name))
val isWebSocket = (req.requestHeaders().contains("Upgrade") || req.requestHeaders().contains("upgrade")) &&
(req.requestHeaders().contains("Sec-WebSocket-Version") || req
.requestHeaders()
.contains("Sec-WebSocket-Version".toLowerCase)) &&
Option(req.requestHeaders().get("Upgrade")).contains("websocket")
ReactiveStreamUtils.FluxUtils.fromFPublisher[Void] {
val version =
sslHandler.map { h => if (h.useH2) "HTTP/2.0" else req.version().toString }.getOrElse(req.version().toString)
val otoReq = new ReactorNettyRequest(
listenerId,
req,
version,
secure,
sessionOpt,
sessionCookieBaker,
flashCookieBaker,
config.exclusive.some
)
val (nreq, reqHandler) = handler.handlerForRequest(otoReq)
reqHandler match {
case a: EssentialAction => {
a.apply(nreq).run(otoReq.body).map { result =>
sendResultAsHttpResponse(result, res)
}
}
case a: WebSocket if isWebSocket => {
a.apply(nreq).map {
case Left(result) => sendResultAsHttpResponse(result, res)
case Right(flow) => {
res.sendWebsocket { (wsInbound, wsOutbound) =>
val processor: Processor[RawMessage, Message] =
WebSocketFlowHandler.webSocketProtocol(65536).join(flow).toProcessor.run()
wsInbound
.receiveFrames()
.map[RawMessage](frameToRawMessage)
.subscribe(processor)
val fluxOut: Flux[WebSocketFrame] = Flux.from(processor).map(messageToFrame)
wsOutbound.sendObject(fluxOut)
}
}
}
}
case a => {
sendResultAsHttpResponse(
Results.InternalServerError(Json.obj("err" -> s"unknown handler: ${a.getClass.getName} - ${a}")),
res
).vfuture
}
}
}
}
private def setupSslContext(): SSLContext = {
new SSLContext(
new SSLContextSpi() {
override def engineCreateSSLEngine(): SSLEngine =
DynamicSSLEngineProvider.createSSLEngine(config.clientAuth, config.cipherSuites, config.protocols, None, env)
override def engineCreateSSLEngine(s: String, i: Int): SSLEngine = engineCreateSSLEngine()
override def engineInit(
keyManagers: Array[KeyManager],
trustManagers: Array[TrustManager],
secureRandom: SecureRandom
): Unit = ()
override def engineGetClientSessionContext(): SSLSessionContext =
DynamicSSLEngineProvider.currentServer.getClientSessionContext
override def engineGetServerSessionContext(): SSLSessionContext =
DynamicSSLEngineProvider.currentServer.getServerSessionContext
override def engineGetSocketFactory(): SSLSocketFactory =
DynamicSSLEngineProvider.currentServer.getSocketFactory
override def engineGetServerSocketFactory(): SSLServerSocketFactory =
DynamicSSLEngineProvider.currentServer.getServerSocketFactory
},
new Provider(
"[NETTY] Otoroshi SSlEngineProvider delegate",
"1.0",
"[NETTY] A provider that delegates calls to otoroshi dynamic one"
) {},
"[NETTY] Otoroshi SSLEngineProvider delegate"
) {}
}
def createEventLoopsOld(): (EventLoopGroup, EventLoopGroup) = {
val groupHttp = EventLoopUtils.create(config.native, config.nThread)
val groupHttps = EventLoopUtils.create(config.native, config.nThread)
groupHttp.native.foreach { name =>
logger.info(s" using ${name} native transport")
logger.info("")
}
(groupHttp.group, groupHttps.group)
}
def createEventLoops(print: Boolean): (LoopResources, LoopResources) = {
val groupHttp =
if (config.nThread == 0) LoopResources.create("otoroshi-http")
else LoopResources.create("otoroshi-http", 2, config.nThread, true)
val groupHttps =
if (config.nThread == 0) LoopResources.create("otoroshi-https")
else LoopResources.create("otoroshi-https", 2, config.nThread, true)
EventLoopUtils.create(config.native, config.nThread).native.foreach { name =>
if (print) {
logger.info(s" using ${name} native transport")
logger.info("")
}
}
(groupHttp, groupHttps)
}
def start(handler: HttpRequestHandler): DisposableReactorNettyServer = {
if (config.enabled) {
if (config.id == HttpListenerNames.Experimental) {
logger.info("")
logger.info(s"Starting the experimental Netty Server !!!")
logger.info("")
}
val (groupHttp, groupHttps) = createEventLoops(config.id == HttpListenerNames.Experimental)
// val (groupHttp: EventLoopGroup, groupHttps: EventLoopGroup) = createEventLoops()
if (config.id == "classic") {
if (config.http3.enabled) logger.info(s" https://${config.host}:${config.http3.port} (HTTP/3)")
logger.info(s" https://${config.host}:${config.httpsPort} (HTTP/1.1, HTTP/2)")
logger.info(s" http://${config.host}:${config.httpPort} (HTTP/1.1, HTTP/2 H2C)")
logger.info("")
}
def handleFunction(
secure: Boolean
): BiFunction[_ >: HttpServerRequest, _ >: HttpServerResponse, _ <: Publisher[Void]] = {
if (config.newEngineOnly) { (req, res) =>
{
val channel = NettyHelper.getChannel(req)
handleHttp(config.id, req, res, secure, channel)
}
} else { (req, res) =>
{
val channel = NettyHelper.getChannel(req)
handle(config.id, req, res, secure, channel, handler)
}
}
}
def applyAddress(socketAddress: SocketAddress) = socketAddress match {
case address: InetSocketAddress => address.getHostString
case _ => "-"
}
val defaultLogFormat = "{} - {} [{}] \"{} {} {}\" {} {} {} {}"
val logCustom = new AccessLogFactory {
override def apply(args: AccessLogArgProvider): AccessLog = {
val tlsVersion = args.requestHeader("Otoroshi-Tls-Version")
AccessLog.create(
defaultLogFormat,
applyAddress(args.remoteAddress()),
args.user(),
args.zonedDateTime(),
args.method(),
args.uri(),
args.protocol(),
args.status(),
if (args.contentLength() > -1L) args.contentLength().toString else "-",
args.duration().toString,
if (tlsVersion == null) "-" else tlsVersion
)
}
}
val protocols = Seq
.empty[HttpProtocol]
.applyOnIf(config.http1.enabled)(_ :+ HttpProtocol.HTTP11)
.applyOnIf(config.http2.enabled || config.http2.h2cEnabled)(_ :+ HttpProtocol.H2C)
val serverHttps =
if (config.httpsPort == -1) None
else
Some(
HttpServer
.create()
.host(config.host)
.accessLog(config.accessLog, logCustom)
.applyOnIf(config.wiretap)(_.wiretap(logger.logger.getName + "-wiretap-https", LogLevel.INFO))
.port(config.httpsPort)
.protocol(protocols: _*)
//.applyOnIf(config.http2.enabled)(_.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C))
//.applyOnIf(!config.http2.enabled)(_.protocol(HttpProtocol.HTTP11))
.runOn(groupHttps)
.httpRequestDecoder(spec =>
spec
.allowDuplicateContentLengths(config.parser.allowDuplicateContentLengths)
.h2cMaxContentLength(config.parser.h2cMaxContentLength)
.initialBufferSize(config.parser.initialBufferSize)
.maxHeaderSize(config.parser.maxHeaderSize)
.maxInitialLineLength(config.parser.maxInitialLineLength)
.maxChunkSize(config.parser.maxChunkSize)
.validateHeaders(config.parser.validateHeaders)
)
.idleTimeout(config.idleTimeout)
.doOnChannelInit { (observer, channel, socket) =>
val engine = setupSslContext().createSSLEngine()
val applicationProtocol = new AtomicReference[String]("http/1.1")
engine.setHandshakeApplicationProtocolSelector((e, protocols) => {
val chosen = protocols match {
case ps if ps.contains("h2") && config.http2.enabled => "h2"
case ps if ps.contains("spdy/3") => "spdy/3"
case _ => "http/1.1"
}
applicationProtocol.set(chosen)
chosen
})
// we do not use .secure() because of no dynamic sni support and use SslHandler instead !
channel.pipeline().addFirst(new OtoroshiSslHandler(engine, applicationProtocol))
channel.pipeline().addLast(new OtoroshiErrorHandler(logger))
}
.handle(handleFunction(true))
.bindNow()
)
val serverHttp =
if (config.httpPort == -1) None
else
Some(
HttpServer
.create()
.host(config.host)
.noSSL()
.accessLog(config.accessLog, logCustom)
.applyOnIf(config.wiretap)(_.wiretap(logger.logger.getName + "-wiretap-http", LogLevel.INFO))
.port(config.httpPort)
.protocol(protocols: _*)
//.applyOnIf(config.http2.h2cEnabled)(_.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C))
//.applyOnIf(!config.http2.h2cEnabled)(_.protocol(HttpProtocol.HTTP11))
.handle(handleFunction(false))
.runOn(groupHttp)
.httpRequestDecoder(spec =>
spec
.allowDuplicateContentLengths(config.parser.allowDuplicateContentLengths)
.h2cMaxContentLength(config.parser.h2cMaxContentLength)
.initialBufferSize(config.parser.initialBufferSize)
.maxHeaderSize(config.parser.maxHeaderSize)
.maxInitialLineLength(config.parser.maxInitialLineLength)
.maxChunkSize(config.parser.maxChunkSize)
.validateHeaders(config.parser.validateHeaders)
)
.idleTimeout(config.idleTimeout)
.bindNow()
)
val http3Server = new NettyHttp3Server(config, env).start(handler, sessionCookieBaker, flashCookieBaker)
val disposableServer = DisposableReactorNettyServer(config.id, serverHttp, serverHttps, http3Server.some)
Runtime.getRuntime.addShutdownHook(new Thread(() => {
disposableServer.stop()
}))
disposableServer
} else {
DisposableReactorNettyServer(config.id, None, None, None)
}
}
}
case class DisposableReactorNettyServer(
name: String,
serverHttp: Option[DisposableServer],
serverHttps: Option[DisposableServer],
http3Server: Option[DisposableNettyHttp3Server]
) {
def stop(): Unit = {
if (
name != HttpListenerNames.Experimental && (serverHttp.isDefined || serverHttps.isDefined || http3Server.isDefined)
) {
HttpListener.logger.info(s"stopping http listener '${name}'")
}
serverHttp.foreach(_.disposeNow())
serverHttps.foreach(_.disposeNow())
http3Server.foreach(_.stop())
}
}
class OtoroshiSslHandler(engine: SSLEngine, appProto: AtomicReference[String]) extends SslHandler(engine) {
def useH2: Boolean = Option(appProto.get()).contains("h2")
def chosenApplicationProtocol: String = Option(appProto.get()).getOrElse("http/1.1")
}
class OtoroshiErrorHandler(logger: Logger) extends ChannelHandler {
override def handlerAdded(ctx: ChannelHandlerContext): Unit = ()
override def handlerRemoved(ctx: ChannelHandlerContext): Unit = ()
override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = {
logger.error("error caught in netty pipeline", cause)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy