import { Observable , Subscription , combineLatest } from 'rxjs' ; import { first , map } from 'rxjs/operators' ; import { pick } from '@kbn/std' ; import type { RequestHandlerContext } from 'src/core/server' ; import type { InternalExecutionContextSetup } from '../execution_context' ; import { CoreService } from '../../types' ; import { Logger } from '../logging' ; import { ContextSetup , InternalContextPreboot } from '../context' ; import { Env } from '../config' ; import { CoreContext } from '../core_context' ; import { PluginOpaqueId } from '../plugins' ; import { CspConfigType , config as cspConfig } from '../csp' ; import { Router } from './router' ; import { HttpConfig , HttpConfigType , config as httpConfig } from './http_config' ; import { HttpServer } from './http_server' ; import { HttpsRedirectServer } from './https_redirect_server' ; import { RequestHandlerContextContainer , RequestHandlerContextProvider , InternalHttpServicePreboot , InternalHttpServiceSetup , InternalHttpServiceStart , } from './types' ; import { registerCoreHandlers } from './lifecycle_handlers' ; import { ExternalUrlConfigType , config as externalUrlConfig , ExternalUrlConfig , } from '../external_url' ; export interface PrebootDeps { context : InternalContextPreboot ; } export interface SetupDeps { context : ContextSetup ; executionContext : InternalExecutionContextSetup ; } internal export class HttpService implements CoreService < InternalHttpServiceSetup , InternalHttpServiceStart > { private readonly prebootServer : HttpServer ; private isPrebootServerStopped = false ; private readonly httpServer : HttpServer ; private readonly httpsRedirectServer : HttpsRedirectServer ; private readonly config$ : Observable < HttpConfig > ; private configSubscription ? : Subscription ; private readonly log : Logger ; private readonly env : Env ; private internalPreboot ? : InternalHttpServicePreboot ; private internalSetup ? : InternalHttpServiceSetup ; private requestHandlerContext ? : RequestHandlerContextContainer ; constructor ( private readonly coreContext ) { const { logger , configService , env } = coreContext ; this . env = env ; this . log = logger . get ( 'http' ) ; this . config$ = combineLatest ( [ configService . atPath < HttpConfigType > ( httpConfig . path ) , configService . atPath < CspConfigType > ( cspConfig . path ) , configService . atPath < ExternalUrlConfigType > ( externalUrlConfig . path ) , ] ) . pipe ( map ( ( [ http , csp , externalUrl ] ) => new HttpConfig ( http , csp , externalUrl ) ) ) ; const shutdownTimeout$ = this . config$ . pipe ( map ( ( { shutdownTimeout } ) => shutdownTimeout ) ) ; this . prebootServer = new HttpServer ( logger , 'Preboot' , shutdownTimeout$ ) ; this . httpServer = new HttpServer ( logger , 'Kibana' , shutdownTimeout$ ) ; this . httpsRedirectServer = new HttpsRedirectServer ( logger . get ( 'http' , 'redirect' , 'server' ) ) ; } public async preboot ( deps ) { this . log . debug ( 'setting up preboot server' ) ; const config = await this . config$ . pipe ( first ( ) ) . toPromise ( ) ; const prebootSetup = await this . prebootServer . setup ( config ) ; prebootSetup . server . route ( { path : '/{p*}' , method : '*' , handler : ( req , responseToolkit ) => { this . log . debug ( `Kibana server is not ready yet ${ req . method }:${ req . url . href }.` ) ; return responseToolkit . response ( 'Kibana server is not ready yet' ) . code ( 503 ) . header ( 'Retry-After' , '30' ) ; } , } ) ; if ( this . shouldListen ( config ) ) { this . log . debug ( 'starting preboot server' ) ; await this . prebootServer . start ( ) ; } const prebootServerRequestHandlerContext = deps . context . createContextContainer ( ) ; this . internalPreboot = { externalUrl : new ExternalUrlConfig ( config . externalUrl ) , csp : prebootSetup . csp , basePath : prebootSetup . basePath , registerStaticDir : prebootSetup . registerStaticDir . bind ( prebootSetup ) , auth : prebootSetup . auth , server : prebootSetup . server , registerRouteHandlerContext : ( pluginOpaqueId , contextName , provider ) => prebootServerRequestHandlerContext . registerContext ( pluginOpaqueId , contextName , provider ) , registerRoutes : ( path , registerCallback ) => { const router = new Router ( path , this . log , prebootServerRequestHandlerContext . createHandler . bind ( null , this . coreContext . coreId ) ) ; registerCallback ( router ) ; prebootSetup . registerRouterAfterListening ( router ) ; } , getServerInfo : prebootSetup . getServerInfo , } ; return this . internalPreboot ; } public async setup ( deps ) { this . requestHandlerContext = deps . context . createContextContainer ( ) ; this . configSubscription = this . config$ . subscribe ( ( ) => { if ( this . httpServer . isListening ( ) ) { this . log . warn ( 'Received new HTTP config after server was started. Config will **not** be applied.' ) ; } } ) ; const config = await this . config$ . pipe ( first ( ) ) . toPromise ( ) ; const { registerRouter , ... serverContract } = await this . httpServer . setup ( config , deps . executionContext ) ; registerCoreHandlers ( serverContract , config , this . env ) ; this . internalSetup = { ... serverContract , externalUrl : new ExternalUrlConfig ( config . externalUrl ) , createRouter : < Context extends RequestHandlerContext = RequestHandlerContext > ( path , pluginId = this . coreContext . coreId ) => { const enhanceHandler = this . requestHandlerContext ! . createHandler . bind ( null , pluginId ) ; const router = new Router < Context > ( path , this . log , enhanceHandler ) ; registerRouter ( router ) ; return router ; } , registerRouteHandlerContext : < Context extends RequestHandlerContext , ContextName extends keyof Context > ( pluginOpaqueId , contextName , provider ) => this . requestHandlerContext ! . registerContext ( pluginOpaqueId , contextName , provider ) , registerPrebootRoutes : this . internalPreboot ! . registerRoutes , } ; return this . internalSetup ; } public getStartContract ( ) { return { ... pick ( this . internalSetup ! , [ 'auth' , 'basePath' , 'getServerInfo' ] ) , isListening : ( ) => this . httpServer . isListening ( ) , } ; } public async start ( ) { const config = await this . config$ . pipe ( first ( ) ) . toPromise ( ) ; if ( this . shouldListen ( config ) ) { this . log . debug ( 'stopping preboot server' ) ; await this . prebootServer . stop ( ) ; this . isPrebootServerStopped = true ; if ( config . ssl . enabled && config . ssl . redirectHttpFromPort !== undefined ) { await this . httpsRedirectServer . start ( config ) ; } await this . httpServer . start ( ) ; } return this . getStartContract ( ) ; } internal private shouldListen ( config ) { return config . autoListen ; } public async stop ( ) { this . configSubscription ?. unsubscribe ( ) ; this . configSubscription = undefined ; if ( ! this . isPrebootServerStopped ) { this . isPrebootServerStopped = false ; await this . prebootServer . stop ( ) ; } await this . httpServer . stop ( ) ; await this . httpsRedirectServer . stop ( ) ; } }