All Downloads are FREE. Search and download functionalities are using the official Maven repository.

spice.http.server.undertow.UndertowServerImplementation.scala Maven / Gradle / Ivy

There is a newer version: 0.6.2
Show newest version
package spice.http.server.undertow

import cats.effect.IO
import io.undertow.{Undertow, UndertowOptions}
import io.undertow.predicate.Predicates
import io.undertow.server.{HttpServerExchange, HttpHandler => UndertowHttpHandler}
import io.undertow.server.handlers.encoding.{ContentEncodingRepository, DeflateEncodingProvider, EncodingHandler, GzipEncodingProvider}
import moduload.Moduload
import reactify._
import scribe.Logger
import spice.http.server.config.{HttpServerListener, HttpsServerListener}
import spice.http.{HttpExchange, HttpResponse}
import spice.http.server.{HttpServer, HttpServerImplementation, HttpServerImplementationManager, SSLUtil, ServerStartException}
import spice.net.{MalformedURLException, URL}

import java.util.logging.LogManager
import cats.effect.unsafe.implicits.global
import scribe.mdc.MDC

import scala.jdk.CollectionConverters.ListHasAsScala

class UndertowServerImplementation(server: HttpServer) extends HttpServerImplementation with UndertowHttpHandler {
  private val instance: Var[Option[Undertow]] = Var(None)

  override def start(server: HttpServer): IO[Unit] = IO {
    val contentEncodingRepository = new ContentEncodingRepository()
      .addEncodingHandler("gzip", new GzipEncodingProvider, 100, Predicates.requestLargerThan(5L))
      .addEncodingHandler("deflate", new DeflateEncodingProvider, 50, Predicates.requestLargerThan(5L))
    val encodingHandler = new EncodingHandler(contentEncodingRepository).setNext(this)

    val builder = Undertow.builder().setHandler(encodingHandler)
    if (server.config.enableHTTP2) {
      builder.setServerOption(UndertowOptions.ENABLE_HTTP2, java.lang.Boolean.TRUE)
    }

    server.config.listeners.foreach {
      case HttpServerListener(host, port, enabled, _, _) => if (enabled) {
        builder.addHttpListener(port.getOrElse(0), host)
      }
      case HttpsServerListener(host, port, keyStore, enabled, _, _) => if (enabled) {
        try {
          val sslContext = SSLUtil.createSSLContext(keyStore.location, keyStore.password)
          builder.addHttpsListener(port.getOrElse(0), host, sslContext)
        } catch {
          case t: Throwable =>
            throw new RuntimeException(s"Error loading HTTPS, host: $host, port: $port, keyStore: ${keyStore.path}", t)
        }
      }
      case listener => throw new UnsupportedOperationException(s"Unsupported listener: $listener")
    }
    val u = builder.build()
    try {
      u.start()
      val urls = u.getListenerInfo.asScala.map { info =>
        val a = info.getAddress.toString.replace("[", "").replace("]", "")
        val address = s"${info.getProtcol}:/$a"
        try {
          Some(URL.parse(address))
        } catch {
          case t: Throwable =>
            scribe.error(s"Failed to parse $address", t)
            None
        }
      }
      server.config.listeners @= server.config.listeners().zip(urls).map {
        case (listener, urlOption) => urlOption match {
          case Some(url) =>
            val host = if (listener.host == "0.0.0.0") {
              url.host
            } else {
              listener.host
            }
            val port = if (listener.port.isEmpty) {
              Some(url.port)
            } else {
              listener.port
            }
            listener match {
              case l: HttpServerListener => l.copy(host = host, port = port)
              case l: HttpsServerListener => l.copy(host = host, port = port)
            }
          case None => listener
        }
      }
    } catch {
      case t: Throwable => throw ServerStartException(t.getMessage, server.config.listeners(), t)
    }
    instance @= Some(u)
  }

  override def isRunning: Boolean = instance().nonEmpty

  override def stop(server: HttpServer): IO[Unit] = IO {
    instance() match {
      case Some(u) =>
        u.stop()
        instance @= None
      case None => // Not running
    }
  }

  override def handleRequest(undertow: HttpServerExchange): Unit = {
    try {
      val url = URL.parse(s"${undertow.getRequestURL}?${undertow.getQueryString}")
      if (!server.config.persistentConnections()) {
        undertow.setPersistent(false)
      }

      undertow.dispatch(new Runnable {
        override def run(): Unit = {
          MDC { implicit mdc =>
            mdc("url") = url
            val io = UndertowRequestParser(undertow, url).flatMap { request =>
              val exchange = HttpExchange(request)
              server.handle(exchange)
                .redeemWith(server.errorRecovery(exchange, _), IO.pure)
            }.flatMap { exchange =>
              exchange.webSocketListener match {
                case Some(webSocketListener) => UndertowWebSocketHandler(undertow, server, exchange, webSocketListener)
                case None => UndertowResponseSender(undertow, server, exchange)
              }
            }.redeemWith({ throwable =>
              scribe.error("Unrecoverable error parsing request!", throwable)
              throw throwable
            }, IO.pure)
            io.unsafeRunAndForget()(server.ioRuntime)
          }
        }
      })
    } catch {
      case exc: MalformedURLException => scribe.warn(exc.message)
      case throwable: Throwable => server.errorLogger(throwable, None, None).unsafeRunAndForget()
    }
  }
}

object UndertowServerImplementation extends Moduload {
  override def load(): Unit = {
    LogManager.getLogManager.reset()
    Logger.system.installJUL()

    HttpServerImplementationManager.register(new UndertowServerImplementation(_))
  }

  override def error(t: Throwable): Unit = {
    scribe.error("Error while attempting to register UndertowServerImplementation", t)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy