import { Observable , Subscription , combineLatest } from 'rxjs' ; import { first , map } from 'rxjs/operators' ; import { Server } from '@hapi/hapi' ; import { pick } from '@osd/std' ; import { CoreService } from '../../types' ; import { Logger , LoggerFactory } from '../logging' ; import { ContextSetup } 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 , InternalHttpServiceSetup , InternalHttpServiceStart , } from './types' ; import { RequestHandlerContext } from '../../server' ; import { registerCoreHandlers } from './lifecycle_handlers' ; interface SetupDeps { context : ContextSetup ; } internal export class HttpService implements CoreService < InternalHttpServiceSetup , InternalHttpServiceStart > { private readonly httpServer : HttpServer ; private readonly httpsRedirectServer : HttpsRedirectServer ; private readonly config$ : Observable < HttpConfig > ; private configSubscription ? : Subscription ; private readonly logger : LoggerFactory ; private readonly log : Logger ; private readonly env : Env ; private notReadyServer ? : Server ; private internalSetup ? : InternalHttpServiceSetup ; private requestHandlerContext ? : RequestHandlerContextContainer ; constructor ( private readonly coreContext ) { const { logger , configService , env } = coreContext ; this . logger = logger ; this . env = env ; this . log = logger . get ( 'http' ) ; this . config$ = combineLatest ( [ configService . atPath < HttpConfigType > ( httpConfig . path ) , configService . atPath < CspConfigType > ( cspConfig . path ) , ] ) . pipe ( map ( ( [ http , csp ] ) => new HttpConfig ( http , csp ) ) ) ; this . httpServer = new HttpServer ( logger , 'OpenSearchDashboards' ) ; this . httpsRedirectServer = new HttpsRedirectServer ( logger . get ( 'http' , 'redirect' , 'server' ) ) ; } 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 ( ) ; if ( this . shouldListen ( config ) ) { await this . runNotReadyServer ( config ) ; } const { registerRouter , ... serverContract } = await this . httpServer . setup ( config ) ; registerCoreHandlers ( serverContract , config , this . env ) ; this . internalSetup = { ... serverContract , createRouter : ( path , pluginId = this . coreContext . coreId ) => { const enhanceHandler = this . requestHandlerContext ! . createHandler . bind ( null , pluginId ) ; const router = new Router ( path , this . log , enhanceHandler ) ; registerRouter ( router ) ; return router ; } , registerRouteHandlerContext : < T extends keyof RequestHandlerContext > ( pluginOpaqueId , contextName , provider ) => this . requestHandlerContext ! . registerContext ( pluginOpaqueId , contextName , provider ) , } ; 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 ) ) { if ( this . notReadyServer ) { this . log . debug ( 'stopping NotReady server' ) ; await this . notReadyServer . stop ( ) ; this . notReadyServer = undefined ; } 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 ! this . coreContext . env . isDevClusterMaster && config . autoListen ; } public async stop ( ) { if ( this . configSubscription === undefined ) { return ; } this . configSubscription . unsubscribe ( ) ; this . configSubscription = undefined ; if ( this . notReadyServer ) { await this . notReadyServer . stop ( ) ; } await this . httpServer . stop ( ) ; await this . httpsRedirectServer . stop ( ) ; } private async runNotReadyServer ( config ) { this . log . debug ( 'starting NotReady server' ) ; const httpServer = new HttpServer ( this . logger , 'NotReady' ) ; const { server } = await httpServer . setup ( config ) ; this . notReadyServer = server ; this . notReadyServer . route ( { path : '/{p*}' , method : '*' , handler : ( req , responseToolkit ) => { this . log . debug ( `OpenSearch Dashboards server is not ready yet ${ req . method }:${ req . url . href }.` ) ; return responseToolkit . response ( 'OpenSearch Dashboards server is not ready yet' ) . code ( 503 ) . header ( 'Retry-After' , '30' ) ; } , } ) ; await this . notReadyServer . start ( ) ; } }