uy.kohesive.kovert.vertx.boot.KovertVerticle.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kovert-vertx-jdk8 Show documentation
Show all versions of kovert-vertx-jdk8 Show documentation
An invisible covert web framework, shhhhhhh.
package uy.kohesive.kovert.vertx.boot
import io.vertx.core.AbstractVerticle
import io.vertx.core.Handler
import io.vertx.core.Vertx
import io.vertx.core.http.HttpMethod
import io.vertx.core.http.HttpServerOptions
import io.vertx.core.logging.Logger
import io.vertx.core.net.JksOptions
import io.vertx.ext.auth.AuthProvider
import io.vertx.ext.web.Route
import io.vertx.ext.web.Router
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.*
import io.vertx.ext.web.sstore.ClusteredSessionStore
import io.vertx.ext.web.sstore.LocalSessionStore
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import uy.klutter.config.typesafe.KonfigModule
import uy.klutter.config.typesafe.*
import uy.klutter.core.common.initializedBy
import uy.klutter.core.jdk.mustEndWith
import uy.klutter.core.jdk.mustNotEndWith
import uy.klutter.core.jdk.mustStartWith
import uy.klutter.core.jdk.nullIfBlank
import uy.klutter.vertx.promiseDeployVerticle
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.*
import java.util.concurrent.TimeUnit
public object KovertVerticleModule : KonfigModule, InjektModule {
override fun KonfigRegistrar.registerConfigurables() {
bindClassAtConfigRoot()
}
override fun InjektRegistrar.registerInjectables() {
}
}
public class KovertVerticle private constructor(val cfg: KovertVerticleConfig, val customization: KovertVerticleCustomization?, val routerInit: Router.() -> Unit, val onListenerReady: (String) -> Unit) : AbstractVerticle() {
companion object {
val LOG: Logger = io.vertx.core.logging.LoggerFactory.getLogger(KovertVerticle::class.java)
/**
* Deploys a KovertVerticle into a Vertx instance and returns a Promise representing the deployment ID.
* The HTTP listeners are active before the promise completes.
*/
public fun deploy(vertx: Vertx, cfg: KovertVerticleConfig = Injekt.get(), customization: KovertVerticleCustomization? = null, routerInit: Router.() -> Unit): Promise {
val deferred = deferred()
val completeThePromise = fun(id: String): Unit {
LOG.warn("KovertVerticle is listening and ready.")
deferred.resolve(id)
}
vertx.promiseDeployVerticle(KovertVerticle(cfg, customization, routerInit, completeThePromise)) success { deploymentId ->
LOG.warn("KovertVerticle deployed as ${deploymentId}")
} fail { failureException ->
LOG.error("Vertx deployment failed due to ${failureException.message}", failureException)
deferred.reject(failureException)
}
return deferred.promise
}
}
override fun start() {
LOG.warn("API Verticle starting")
cfg.verify(LOG)
customization?.verify(LOG)
val cookieHandler = CookieHandler.create()
fun cookieHandlerFactory() = cookieHandler
val sessionStore = if (vertx.isClustered()) ClusteredSessionStore.create(vertx) else LocalSessionStore.create(vertx)
val sessionHandler = SessionHandler.create(sessionStore).setSessionTimeout(TimeUnit.HOURS.toMillis(cfg.sessionTimeoutInHours.toLong())).setNagHttps(false)
try {
val appRouter = Router.router(vertx) initializedBy { router ->
router.route().handler(LoggerHandler.create())
fun applyHandlerToRoutePrefixes(handle: Handler, prefixes: List, methods: List = emptyList()) {
if (prefixes.isEmpty()) {
val temp = router.route()
methods.forEach { temp.method(it) }
temp.handler(handle)
} else {
prefixes.forEach {
val temp = router.route(it.mustStartWith('/').mustEndWith("/*"))
methods.forEach { temp.method(it) }
temp.handler(handle)
}
}
}
// setup CORS early, so that it prevents other code from running that shouldn't on CORS preflight checks
if (customization != null && customization.corsHandler != null) {
applyHandlerToRoutePrefixes(customization.corsHandler, customization.corsHandlerRoutePrefixes)
}
// body handle needs to be very early, or there is a chance the body is received before it is setup to consume it
val bodyHandler = BodyHandler.create().setBodyLimit(customization?.bodyHandlerSizeLimit ?: DEFAULT_BODY_HANDLER_LIMIT)
applyHandlerToRoutePrefixes(bodyHandler, customization?.bodyHandlerRoutePrefixes ?: emptyList(),
listOf(HttpMethod.PUT, HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PATCH))
// TODO: we shouldn't waste effort put cookies on API routes
router.route().handler(cookieHandlerFactory())
// TODO: same for session handling, we are creating a new session for each API call (because most callers drop cookies on the floor)
router.route().method(HttpMethod.GET).method(HttpMethod.PUT).method(HttpMethod.POST).method(HttpMethod.DELETE).method(HttpMethod.PATCH).handler(sessionHandler)
// TODO: which of the auth providers should be in the API routes, vs. Web + public
// authentication
if (customization != null && customization.authProvider != null) {
val userSessionHandler = UserSessionHandler.create(customization.authProvider)
applyHandlerToRoutePrefixes(userSessionHandler, emptyList(),
listOf(HttpMethod.GET, HttpMethod.PUT, HttpMethod.POST, HttpMethod.DELETE, HttpMethod.PATCH))
}
// auth handler (checks login, if not does something)
if (customization != null && customization.authHandler != null) {
applyHandlerToRoutePrefixes(customization.authHandler, customization.authHandlerRoutePrefixes)
}
// give the user a chance to bind more routes, for example their controllers
router.routerInit()
cfg.publicDirs.forEach { mountPoint ->
val mountPath = mountPoint.mountAt.mustStartWith('/').mustNotEndWith('/')
val mountRoute = if (mountPath.isBlank() || mountPath == "/") {
router.route()
} else {
router.route(mountPath)
}
LOG.info("Mounting static asset ${mountPoint.dir} at route ${mountPath}")
val mountHandler = StaticHandler.create(mountPoint.dir)
// TODO: allow configuration of cache control headers for StaticHandler
mountRoute.handler(mountHandler)
}
}
cfg.listeners.forEach { listenCfg ->
val httpOptions = HttpServerOptions()
val scheme = if (listenCfg.ssl != null && listenCfg.ssl.enabled) {
httpOptions.setSsl(true).setKeyStoreOptions(JksOptions()
.setPath(listenCfg.ssl.keyStorePath!!)
.setPassword(listenCfg.ssl.keyStorePassword.nullIfBlank()))
"HTTPS"
} else {
"HTTP"
}
vertx.createHttpServer(httpOptions).requestHandler { appRouter.accept(it) }.listen(listenCfg.port)
LOG.warn("${scheme}:${listenCfg.port} server started and ready.")
}
LOG.warn("API Verticle successfully started")
onListenerReady(deploymentID())
} catch (ex: Throwable) {
LOG.error("API Verticle failed to start, fatal error. ${ex.message}", ex)
// terminate the app
throw RuntimeException("Fatal startup error", ex)
}
}
}
internal val DEFAULT_BODY_HANDLER_LIMIT: Long = 32 * 1024
public data class KovertVerticleCustomization(
val bodyHandlerRoutePrefixes: List = emptyList(),
val bodyHandlerSizeLimit: Long = DEFAULT_BODY_HANDLER_LIMIT,
val corsHandler: CorsHandler? = null,
val corsHandlerRoutePrefixes: List = emptyList(),
val authProvider: AuthProvider? = null,
val authHandler: AuthHandler? = null,
val authHandlerRoutePrefixes: List = emptyList()) {
fun verify(LOG: Logger) {
// TODO: check that the right combination of parameters exist or throw exception
}
}
public data class KovertVerticleConfig(val listeners: List,
val publicDirs: List,
val sessionTimeoutInHours: Int) {
fun verify(LOG: Logger) {
// TODO: check that the right combination of parameters exist or throw exception
}
}
public data class HttpListenerConfig(val host: String, val port: Int, val ssl: HttpSslConfig?)
public data class HttpSslConfig(val enabled: Boolean, val keyStorePath: String?, val keyStorePassword: String?)
public data class DirMountConfig(val mountAt: String, val dir: String)