import { Server } from '@hapi/hapi' ; import HapiStaticFiles from '@hapi/inert' ; import url from 'url' ; import uuid from 'uuid' ; import { Logger , LoggerFactory } from '../logging' ; import { HttpConfig } from './http_config' ; import { createServer , getListenerOptions , getServerOptions , getRequestId } from './http_tools' ; import { adoptToHapiAuthFormat , AuthenticationHandler } from './lifecycle/auth' ; import { adoptToHapiOnPreAuth , OnPreAuthHandler } from './lifecycle/on_pre_auth' ; import { adoptToHapiOnPostAuthFormat , OnPostAuthHandler } from './lifecycle/on_post_auth' ; import { adoptToHapiOnRequest , OnPreRoutingHandler } from './lifecycle/on_pre_routing' ; import { adoptToHapiOnPreResponseFormat , OnPreResponseHandler } from './lifecycle/on_pre_response' ; import { IRouter , RouteConfigOptions , OpenSearchDashboardsRouteOptions , OpenSearchDashboardsRequestState , isSafeMethod , } from './router' ; import { SessionStorageCookieOptions , createCookieSessionStorageFactory , } from './cookie_session_storage' ; import { IsAuthenticated , AuthStateStorage , GetAuthState } from './auth_state_storage' ; import { AuthHeadersStorage , GetAuthHeaders } from './auth_headers_storage' ; import { BasePath } from './base_path_service' ; import { HttpServiceSetup , HttpServerInfo } from './types' ; internal export interface HttpServerSetup { server : Server ; param router IRouter - a router with registered route handlers.
      registerRouter : ( router : IRouter ) => void ; registerStaticDir : ( path : string , dirPath : string ) => void ; basePath : HttpServiceSetup [ 'basePath' ] ; csp : HttpServiceSetup [ 'csp' ] ; createCookieSessionStorageFactory : HttpServiceSetup [ 'createCookieSessionStorageFactory' ] ; registerOnPreRouting : HttpServiceSetup [ 'registerOnPreRouting' ] ; registerOnPreAuth : HttpServiceSetup [ 'registerOnPreAuth' ] ; registerAuth : HttpServiceSetup [ 'registerAuth' ] ; registerOnPostAuth : HttpServiceSetup [ 'registerOnPostAuth' ] ; registerOnPreResponse : HttpServiceSetup [ 'registerOnPreResponse' ] ; getAuthHeaders : GetAuthHeaders ; auth : { get : GetAuthState ; isAuthenticated : IsAuthenticated ; } ; getServerInfo : ( ) => HttpServerInfo ; } internal export type LifecycleRegistrar = Pick < HttpServerSetup , 'registerOnPreRouting' | 'registerOnPreAuth' | 'registerAuth' | 'registerOnPostAuth' | 'registerOnPreResponse' > ; export class HttpServer { private server ? : Server ; private config ? : HttpConfig ; private registeredRouters = new Set < IRouter > ( ) ; private authRegistered = false ; private cookieSessionStorageCreated = false ; private stopped = false ; private readonly log : Logger ; private readonly authState : AuthStateStorage ; private readonly authRequestHeaders : AuthHeadersStorage ; private readonly authResponseHeaders : AuthHeadersStorage ; constructor ( private readonly logger , private readonly name ) { this . authState = new AuthStateStorage ( ( ) => this . authRegistered ) ; this . authRequestHeaders = new AuthHeadersStorage ( ) ; this . authResponseHeaders = new AuthHeadersStorage ( ) ; this . log = logger . get ( 'http' , 'server' , name ) ; } public isListening ( ) { return this . server !== undefined && this . server . listener . listening ; } private registerRouter ( router ) { if ( this . isListening ( ) ) { throw new Error ( 'Routers can be registered only when HTTP server is stopped.' ) ; } this . registeredRouters . add ( router ) ; } public async setup ( config ) { const serverOptions = getServerOptions ( config ) ; const listenerOptions = getListenerOptions ( config ) ; this . server = createServer ( serverOptions , listenerOptions ) ; await this . server . register ( [ HapiStaticFiles ] ) ; this . config = config ; const basePathService = new BasePath ( config . basePath ) ; this . setupBasePathRewrite ( config , basePathService ) ; this . setupConditionalCompression ( config ) ; this . setupRequestStateAssignment ( config ) ; return { registerRouter : this . registerRouter . bind ( this ) , registerStaticDir : this . registerStaticDir . bind ( this ) , registerOnPreRouting : this . registerOnPreRouting . bind ( this ) , registerOnPreAuth : this . registerOnPreAuth . bind ( this ) , registerAuth : this . registerAuth . bind ( this ) , registerOnPostAuth : this . registerOnPostAuth . bind ( this ) , registerOnPreResponse : this . registerOnPreResponse . bind ( this ) , createCookieSessionStorageFactory : < T > ( cookieOptions ) => this . createCookieSessionStorageFactory ( cookieOptions , config . basePath ) , basePath : basePathService , csp : config . csp , auth : { get : this . authState . get , isAuthenticated : this . authState . isAuthenticated , } , getAuthHeaders : this . authRequestHeaders . get , getServerInfo : ( ) => ( { name : config . name , hostname : config . host , port : config . port , protocol : this . server ! . info . protocol , } ) , server : this . server , } ; } public async start ( ) { if ( this . server === undefined ) { throw new Error ( 'Http server is not setup up yet' ) ; } if ( this . stopped ) { this . log . warn ( `start called after stop` ) ; return ; } this . log . debug ( 'starting http server' ) ; for ( const router of this . registeredRouters ) { for ( const route of router . getRoutes ( ) ) { this . log . debug ( `registering route handler for [${ route . path }]` ) ; const validate = isSafeMethod ( route . method ) ? undefined : { payload : true } ; const { authRequired , tags , body = { } , timeout } = route . options ; const { accepts : allow , maxBytes , output , parse } = body ; const opensearchDashboardsRouteOptions = { xsrfRequired : route . options . xsrfRequired ?? ! isSafeMethod ( route . method ) , } ; this . server . route ( { handler : route . handler , method : route . method , path : route . path , options : { auth : this . getAuthOption ( authRequired ) , app : opensearchDashboardsRouteOptions , tags : tags ? Array . from ( tags ) : undefined , validate , payload : [ allow , maxBytes , output , parse , timeout ?. payload ] . some ( ( x ) => x !== undefined ) ? { allow , maxBytes , output , parse , timeout : timeout ?. payload , multipart : true , } : undefined , timeout : { socket : timeout ?. idleSocket ?? this . config ! . socketTimeout , } , } , } ) ; } } await this . server . start ( ) ; const serverPath = this . config && this . config . rewriteBasePath && this . config . basePath !== undefined ? this . config . basePath : '' ; this . log . info ( `http server running at ${ this . server . info . uri }${ serverPath }` ) ; } public async stop ( ) { this . stopped = true ; if ( this . server === undefined ) { return ; } const hasStarted = this . server . info . started > 0 ; if ( hasStarted ) { this . log . debug ( 'stopping http server' ) ; await this . server . stop ( ) ; } } private getAuthOption ( authRequired = true ) { if ( this . authRegistered === false ) return undefined ; if ( authRequired === true ) { return { mode : 'required' } ; } if ( authRequired === 'optional' ) { return { mode : 'optional' } ; } if ( authRequired === false ) { return false ; } } private setupBasePathRewrite ( config , basePathService ) { if ( config . basePath === undefined || ! config . rewriteBasePath ) { return ; } this . registerOnPreRouting ( ( request , response , toolkit ) => { const oldUrl = request . url . pathname + request . url . search ; const newURL = basePathService . remove ( oldUrl ) ; const shouldRedirect = newURL !== oldUrl ; if ( shouldRedirect ) { return toolkit . rewriteUrl ( newURL ) ; } return response . notFound ( ) ; } ) ; } private setupConditionalCompression ( config ) { if ( this . server === undefined ) { throw new Error ( 'Server is not created yet' ) ; } if ( this . stopped ) { this . log . warn ( `setupConditionalCompression called after stop` ) ; } const { enabled , referrerWhitelist : list } = config . compression ; if ( ! enabled ) { this . log . debug ( 'HTTP compression is disabled' ) ; this . server . ext ( 'onRequest' , ( request , h ) => { request . info . acceptEncoding = '' ; return h . continue ; } ) ; } else if ( list ) { this . log . debug ( `HTTP compression is only enabled for any referrer in the following: ${ list }` ) ; this . server . ext ( 'onRequest' , ( request , h ) => { const { referrer } = request . info ; if ( referrer !== '' ) { const { hostname } = url . parse ( referrer ) ; if ( ! hostname || ! list . includes ( hostname ) ) { request . info . acceptEncoding = '' ; } } return h . continue ; } ) ; } } private setupRequestStateAssignment ( config ) { this . server ! . ext ( 'onRequest' , ( request , responseToolkit ) => { request . app = { ... ( request . app ?? { } ) , requestId : getRequestId ( request , config . requestId ) , requestUuid : uuid . v4 ( ) , } as OpenSearchDashboardsRequestState ; return responseToolkit . continue ; } ) ; } private registerOnPreAuth ( fn ) { if ( this . server === undefined ) { throw new Error ( 'Server is not created yet' ) ; } if ( this . stopped ) { this . log . warn ( `registerOnPreAuth called after stop` ) ; } this . server . ext ( 'onPreAuth' , adoptToHapiOnPreAuth ( fn , this . log ) ) ; } private registerOnPostAuth ( fn ) { if ( this . server === undefined ) { throw new Error ( 'Server is not created yet' ) ; } if ( this . stopped ) { this . log . warn ( `registerOnPostAuth called after stop` ) ; } this . server . ext ( 'onPostAuth' , adoptToHapiOnPostAuthFormat ( fn , this . log ) ) ; } private registerOnPreRouting ( fn ) { if ( this . server === undefined ) { throw new Error ( 'Server is not created yet' ) ; } if ( this . stopped ) { this . log . warn ( `registerOnPreRouting called after stop` ) ; } this . server . ext ( 'onRequest' , adoptToHapiOnRequest ( fn , this . log ) ) ; } private registerOnPreResponse ( fn ) { if ( this . server === undefined ) { throw new Error ( 'Server is not created yet' ) ; } if ( this . stopped ) { this . log . warn ( `registerOnPreResponse called after stop` ) ; } this . server . ext ( 'onPreResponse' , adoptToHapiOnPreResponseFormat ( fn , this . log ) ) ; } private async createCookieSessionStorageFactory < T > ( cookieOptions , basePath ? ) { if ( this . server === undefined ) { throw new Error ( 'Server is not created yet' ) ; } if ( this . stopped ) { this . log . warn ( `createCookieSessionStorageFactory called after stop` ) ; } if ( this . cookieSessionStorageCreated ) { throw new Error ( 'A cookieSessionStorageFactory was already created' ) ; } this . cookieSessionStorageCreated = true ; const sessionStorageFactory = await createCookieSessionStorageFactory < T > ( this . logger . get ( 'http' , 'server' , this . name , 'cookie-session-storage' ) , this . server , cookieOptions , basePath ) ; return sessionStorageFactory ; } private registerAuth < T > ( fn ) { if ( this . server === undefined ) { throw new Error ( 'Server is not created yet' ) ; } if ( this . stopped ) { this . log . warn ( `registerAuth called after stop` ) ; } if ( this . authRegistered ) { throw new Error ( 'Auth interceptor was already registered' ) ; } this . authRegistered = true ; this . server . auth . scheme ( 'login' , ( ) => ( { authenticate : adoptToHapiAuthFormat ( fn , this . log , ( req , { state , requestHeaders , responseHeaders } ) => { this . authState . set ( req , state ) ; if ( responseHeaders ) { this . authResponseHeaders . set ( req , responseHeaders ) ; } if ( requestHeaders ) { this . authRequestHeaders . set ( req , requestHeaders ) ; Object . assign ( req . headers , requestHeaders ) ; } } ) , } ) ) ; this . server . auth . strategy ( 'session' , 'login' ) ; this . server . auth . default ( 'session' ) ; this . registerOnPreResponse ( ( request , preResponseInfo , t ) => { const authResponseHeaders = this . authResponseHeaders . get ( request ) ; return t . next ( { headers : authResponseHeaders } ) ; } ) ; } private registerStaticDir ( path , dirPath ) { if ( this . server === undefined ) { throw new Error ( 'Http server is not setup up yet' ) ; } if ( this . stopped ) { this . log . warn ( `registerStaticDir called after stop` ) ; } this . server . route ( { path , method : 'GET' , handler : { directory : { path : dirPath , listing : false , lookupCompressed : true , } , } , options : { auth : false } , } ) ; } }