{"version":3,"sources":["../../../packages/core/security/connection-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,MAAM,CAAC;AAGlD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAG/D,OAAO,EAAE,cAAc,EAAe,MAAM,yBAAyB,CAAC;AAEtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAKrE,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAqB,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC3B;;OAEG;IACH,UAAU,EAAE,UAAU,CAAC;IAEvB;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,YAAY,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAE9B;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACjC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,IAAI,EAAE,wBAAwB,CAAC;IAE/B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,oBAAY,wBAAwB;IAChC;;OAEG;IACH,MAAM,IAAI;IAEV;;OAEG;IACH,OAAO,IAAI;IAEX;;OAEG;IACH,YAAY,IAAI;IAEhB;;OAEG;IACH,KAAK,IAAI;IAET;;OAEG;IACH,KAAK,IAAI;IAET;;OAEG;IACH,OAAO,IAAI;IAEX;;OAEG;IACH,SAAS,IAAI;CAChB;AAED,MAAM,WAAW,sBAAuB,SAAQ,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC;IAClE,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAYrB,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,iBAAiB;IACzB,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,iBAAiB;IACzB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,eAAe;IAjB3B,OAAO,CAAC,cAAc,CAAmC;IACzD,OAAO,CAAC,iBAAiB,CAA8D;IACvF,OAAO,CAAC,mBAAmB,CAAiB;IAE5C;;;;;OAKG;gBAES,GAAG,EAAE,GAAG,EACR,iBAAiB,EAAE,iBAAiB,EACpC,oBAAoB,EAAE,oBAAoB,EAC1C,iBAAiB,EAAE,iBAAiB,EACpC,cAAc,EAAE,cAAc,EAC9B,oBAAoB,EAAE,oBAAoB,EAC1C,eAAe,EAAE,oBAAoB;IASjD;;;OAGG;IACI,iBAAiB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAAC,cAAc,CAAC;IA6D5E;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAmH3B;;;;OAIG;IACH,OAAO,CAAC,iCAAiC;IAwBzC;;;;OAIG;IACH,OAAO,CAAC,iCAAiC;IA0BzC;;OAEG;IACH,OAAO,CAAC,8BAA8B;IAgBtC;;OAEG;IACH,OAAO,CAAC,6BAA6B;CAexC","file":"connection-stream.d.ts","sourcesContent":["import { Observable, of, throwError } from 'rxjs';\r\nimport { AjaxError } from 'rxjs/ajax';\r\nimport { catchError, map, tap } from 'rxjs/operators';\r\nimport { ExtensionBrokerQuery } from '../data/extension-broker/extension-broker';\r\nimport { GatewayConnection } from '../data/gateway-connection';\r\nimport { HttpStatusCode } from '../data/http-constants';\r\nimport { Net } from '../data/net';\r\nimport { NodeConnection, NodeRequest } from '../data/node-connection';\r\nimport { PowerShellCommand } from '../data/powershell';\r\nimport { PowerShellConnection } from '../data/powershell-connection';\r\nimport { LogLevel } from '../diagnostics/log-level';\r\nimport { Logging } from '../diagnostics/logging';\r\nimport { Strings } from '../generated/strings';\r\nimport { EnvironmentModule, EnvironmentModuleConnectionStatusProvider, ExtensionMethodIdentifier } from '../manifest/environment-modules';\r\nimport { Rpc } from '../rpc/rpc';\r\nimport { AuthorizationManager } from './authorization-manager';\r\nimport { Connection, ConnectionUtility } from './connection';\r\nimport { ConnectionManager } from './connection-manager';\r\n\r\n/**\r\n * Live connection interface.\r\n */\r\nexport interface LiveConnection {\r\n    /**\r\n     * the connection object.\r\n     */\r\n    connection: Connection;\r\n\r\n    /**\r\n     * the loading state while query the connection status.\r\n     */\r\n    loading: boolean;\r\n\r\n    /**\r\n     * the date number (Date.now() value).\r\n     */\r\n    lastUpdated: number;\r\n\r\n    /**\r\n     * The PowerShell connection.\r\n     */\r\n    isPowerShell: boolean;\r\n\r\n    /**\r\n     * The endpoint of PowerShell.\r\n     */\r\n    powerShellEndpoint: string;\r\n\r\n    /**\r\n     * the status of connection.\r\n     */\r\n    status?: LiveConnectionStatus;\r\n\r\n    /**\r\n     * The extra properties on the connection.\r\n     */\r\n    properties?: MsftSme.StringMap<any>;\r\n}\r\n\r\n/**\r\n * The live connection status.\r\n */\r\nexport interface LiveConnectionStatus {\r\n    /**\r\n     * The display string of status.\r\n     */\r\n    label?: string;\r\n\r\n    /**\r\n     * The status type.\r\n     */\r\n    type: LiveConnectionStatusType;\r\n\r\n    /**\r\n     * The detail connection error message.\r\n     */\r\n    details?: string;\r\n}\r\n\r\n/**\r\n * The live connection status type.\r\n */\r\nexport enum LiveConnectionStatusType {\r\n    /**\r\n     * Online status.\r\n     */\r\n    Online = 0,\r\n\r\n    /**\r\n     * Warning status.\r\n     */\r\n    Warning = 1,\r\n\r\n    /**\r\n     * Unauthorized status.\r\n     */\r\n    Unauthorized = 2,\r\n\r\n    /**\r\n     * Error status.\r\n     */\r\n    Error = 3,\r\n\r\n    /**\r\n     * Fatal status.\r\n     */\r\n    Fatal = 4,\r\n\r\n    /**\r\n     * Unknown status (used for loading status).\r\n     */\r\n    Unknown = 5,\r\n\r\n    /**\r\n     * Forbidden status.\r\n     */\r\n    Forbidden = 6\r\n}\r\n\r\nexport interface ConnectionStatusResult extends MsftSme.StringMap<any> {\r\n    status?: LiveConnectionStatus;\r\n    aliases?: string[];\r\n}\r\n\r\n/**\r\n * ConnectionStream class that enables to get all connections once and listen to the change.\r\n *\r\n * TODO:\r\n * 1. Support live connection status for a single connection in such a way that one could subscribe to it from ActiveConnection\r\n *    with that observable always being for the active connection.\r\n * 2. Support updating all connection status on an interval. (using existing expiration field in cache)\r\n * 3. Support preserving status across sessions during the interval time.\r\n *    currently we are using session storage, this may also require credentials to be preserved across sessions.\r\n */\r\nexport class ConnectionStream {\r\n    private statusLabelMap: { [index: number]: string } = {};\r\n    private connectionStrings = MsftSme.getStrings<Strings>().MsftSmeShell.Core.Connection;\r\n    private cacheLiveConnection: LiveConnection;\r\n\r\n    /**\r\n     * Initializes a new instance of the ConnectionStream class.\r\n     * @param connectionManager the connection manager object.\r\n     * @param powershellConnection the powerShell connection object.\r\n     * @param gatewayConnection the gateway connection object.\r\n     */\r\n    constructor(\r\n        private rpc: Rpc,\r\n        private connectionManager: ConnectionManager,\r\n        private powershellConnection: PowerShellConnection,\r\n        private gatewayConnection: GatewayConnection,\r\n        private nodeConnection: NodeConnection,\r\n        private authorizationManager: AuthorizationManager,\r\n        private extensionBroker: ExtensionBrokerQuery) {\r\n        this.statusLabelMap[LiveConnectionStatusType.Error] = this.connectionStrings.ErrorState.label;\r\n        this.statusLabelMap[LiveConnectionStatusType.Fatal] = this.connectionStrings.FatalState.label;\r\n        this.statusLabelMap[LiveConnectionStatusType.Online] = this.connectionStrings.OnlineState.label;\r\n        this.statusLabelMap[LiveConnectionStatusType.Unauthorized] = this.connectionStrings.NeedsAuthorizationState.label;\r\n        this.statusLabelMap[LiveConnectionStatusType.Unknown] = this.connectionStrings.UnknownState.label;\r\n        this.statusLabelMap[LiveConnectionStatusType.Warning] = this.connectionStrings.WarningState.label;\r\n    }\r\n\r\n    /**\r\n     * Wraps a connection in a live connection object by retrieving its current status\r\n     * @param connection the connection object.\r\n     */\r\n    public getLiveConnection(connection: Connection): Observable<LiveConnection> {\r\n        // get the connection types status provider\r\n        const typeInfo = ConnectionUtility.getConnectionTypeInfo(connection);\r\n        if (typeInfo && typeInfo.provider && typeInfo.provider.connectionStatusProvider) {\r\n            const now = Date.now();\r\n            const statusProvider = typeInfo.provider.connectionStatusProvider;\r\n\r\n            if (statusProvider.skipStatusCheck) {\r\n                return of(<LiveConnection>{\r\n                    connection: connection,\r\n                    loading: false,\r\n                    status: {\r\n                        type: LiveConnectionStatusType.Online\r\n                    },\r\n                    isPowerShell: false,\r\n                    powerShellEndpoint: null\r\n                });\r\n            }\r\n\r\n            if (this.cacheLiveConnection\r\n                && this.cacheLiveConnection.status.type === LiveConnectionStatusType.Online\r\n                && this.cacheLiveConnection.connection\r\n                && this.cacheLiveConnection.connection.name === connection.name\r\n                && this.cacheLiveConnection.connection.type === connection.type\r\n                && now - this.cacheLiveConnection.lastUpdated < 2000) {\r\n                return of(this.cacheLiveConnection);\r\n            } else {\r\n                return this.getConnectionStatus(statusProvider, connection, connection.name)\r\n                    .pipe(\r\n                        map(data => {\r\n                            this.cacheLiveConnection = data;\r\n                            return data;\r\n                        }));\r\n            }\r\n        }\r\n\r\n        // this should not happen, it means we have a malformed manifest or the user has uninstalled the relevant connection manager extension.\r\n        const logMessage = this.connectionStrings.NoStatusProvider.message.format(connection.type);\r\n\r\n        // log warning about this condition\r\n        Logging.log({\r\n            source: 'ConnectionStream',\r\n            level: LogLevel.Warning,\r\n            message: logMessage\r\n        });\r\n\r\n        // we dont need to fail, we can set this items status to unknown and continue\r\n        const statusLabel = this.connectionStrings.NoStatusProvider.label;\r\n        return of(<LiveConnection>{\r\n            connection: connection,\r\n            loading: false,\r\n            status: {\r\n                label: statusLabel,\r\n                type: LiveConnectionStatusType.Unknown,\r\n                details: logMessage\r\n            },\r\n            isPowerShell: false,\r\n            powerShellEndpoint: null\r\n        });\r\n    }\r\n\r\n    /**\r\n     * Get connection status and aliases from statusProvider, retry alaises when connection nodeName NotFound\r\n     * @param statusProvider status provider from typeInfo manifest\r\n     * @param connection original connection\r\n     * @param nodeName retry nodeName, can be connection.name or alias\r\n     */\r\n    private getConnectionStatus(\r\n        statusProvider: EnvironmentModuleConnectionStatusProvider,\r\n        connection: Connection,\r\n        nodeName: string): Observable<LiveConnection> {\r\n        // collect the status data from the status provider\r\n        let statusAwaiter: Observable<ConnectionStatusResult>;\r\n        if (statusProvider.powerShell) {\r\n            statusAwaiter = this.getConnectionStatusFromPowershell(nodeName, statusProvider.powerShell);\r\n        } else if (statusProvider.relativeGatewayUrl) {\r\n            statusAwaiter = this.getConnectionStatusFromGatewayUrl(nodeName, statusProvider.relativeGatewayUrl);\r\n        } else if (statusProvider.service) {\r\n            statusAwaiter = this.getConnectionStatusFromService(nodeName, statusProvider.service);\r\n        } else if (statusProvider.worker) {\r\n            statusAwaiter = this.getConnectionStatusFromWorker(nodeName, statusProvider.worker);\r\n        }\r\n\r\n        return statusAwaiter\r\n            .pipe(\r\n                map(statusData => {\r\n                    // create connection object to return\r\n                    const liveConnection: LiveConnection = {\r\n                        connection: connection,\r\n                        loading: false,\r\n                        lastUpdated: Date.now(),\r\n                        isPowerShell: !!statusProvider.powerShell,\r\n                        powerShellEndpoint: statusProvider.powerShell && this.authorizationManager.getJeaEndpoint(connection.name)\r\n                    };\r\n                    // extract the status field from the data\r\n                    if (statusData.status) {\r\n                        liveConnection.status = statusData.status;\r\n                        delete statusData.status;\r\n                    } else {\r\n                        // if there is no status field, assume everything is good since we got this far.\r\n                        liveConnection.status = {\r\n                            type: LiveConnectionStatusType.Online\r\n                        };\r\n                    }\r\n                    // load localized values for label and details\r\n                    if (statusProvider.displayValueMap) {\r\n                        const label = statusProvider.displayValueMap[liveConnection.status.label];\r\n                        const details = statusProvider.displayValueMap[liveConnection.status.details];\r\n                        liveConnection.status.label = label || liveConnection.status.label;\r\n                        liveConnection.status.details = details || liveConnection.status.details;\r\n                    }\r\n                    // extract the aliases field from the data\r\n                    if (statusData.aliases) {\r\n                        this.connectionManager.saveAliasesData(statusData.aliases, connection, nodeName);\r\n                        delete statusData.aliases;\r\n                    }\r\n                    // any other properties returned by status call live in property bag.\r\n                    liveConnection.properties = statusData;\r\n\r\n                    // fill properties of connection with status data.\r\n                    connection.properties = connection.properties || {};\r\n                    Object.assign(connection.properties, statusData);\r\n\r\n                    // return the new live connection\r\n                    return liveConnection;\r\n                }),\r\n                catchError((error: AjaxError) => {\r\n                    const liveConnection = <LiveConnection>{\r\n                        connection: connection,\r\n                        loading: false,\r\n                        lastUpdated: Date.now(),\r\n                        isPowerShell: !!statusProvider.powerShell,\r\n                        powerShellEndpoint: statusProvider.powerShell && this.authorizationManager.getJeaEndpoint(connection.name)\r\n                    };\r\n                    if (Net.isUnauthorized(error)) {\r\n                        liveConnection.status = {\r\n                            type: LiveConnectionStatusType.Unauthorized\r\n                        };\r\n                    } else if (Net.isForbidden(error)) {\r\n                        liveConnection.status = {\r\n                            type: LiveConnectionStatusType.Forbidden\r\n                        };\r\n                    } else if (error.status === HttpStatusCode.NotFound || error.status === HttpStatusCode.BadRequest) {\r\n                        // failed connect to target node\r\n                        const alias = this.connectionManager.getActiveAlias(connection.name);\r\n                        // found alias, retry\r\n                        if (alias) {\r\n                            return this.getConnectionStatus(statusProvider, connection, alias);\r\n                        } else { // no alias, return error\r\n\r\n                            // For Badrequest(400), try to extract the PS error code.\r\n                            if (error.status === HttpStatusCode.BadRequest) {\r\n                                liveConnection.status = {\r\n                                    type: LiveConnectionStatusType.Fatal,\r\n                                    details: Net.getErrorMessage(error)\r\n                                };\r\n                            } else { // else just show the default connection failure error.\r\n                                const label = this.connectionStrings.NoConnection.label;\r\n                                const message = this.connectionStrings.NoConnection.message;\r\n                                liveConnection.status = {\r\n                                    label: label,\r\n                                    type: LiveConnectionStatusType.Unknown,\r\n                                    details: message\r\n                                };\r\n                            }\r\n                        }\r\n                    } else {\r\n                        liveConnection.status = {\r\n                            type: LiveConnectionStatusType.Fatal,\r\n                            details: Net.getErrorMessage(error)\r\n                        };\r\n                    }\r\n                    return of(liveConnection);\r\n                }),\r\n                tap(liveConnection => {\r\n                    if (!liveConnection.status.label) {\r\n                        liveConnection.status.label = this.statusLabelMap[liveConnection.status.type]\r\n                            || this.statusLabelMap[LiveConnectionStatusType.Unknown];\r\n                    }\r\n                }));\r\n    }\r\n\r\n    /**\r\n     * Retrieves a connections status from a powershell status provider\r\n     * @param nodeName the node name.\r\n     * @param options The powershell options.\r\n     */\r\n    private getConnectionStatusFromPowershell(\r\n        nodeName: string,\r\n        options: PowerShellCommand): Observable<ConnectionStatusResult> {\r\n        if (!nodeName) {\r\n            // log warning about this condition\r\n            const message = this.connectionStrings.ErrorNodeName.message;\r\n            Logging.log({\r\n                source: 'ConnectionStream',\r\n                level: LogLevel.Error,\r\n                message: message.format(nodeName)\r\n            });\r\n            return throwError(() => new Error(message));\r\n        }\r\n\r\n        const psSession = this.powershellConnection.createAutomaticSession(nodeName, { noAuth: true });\r\n        return this.powershellConnection.run(psSession, options.command ? options : options.script)\r\n            .pipe(\r\n                map(response => {\r\n                    // we expect the script to return an object as the first result.\r\n                    // The object should have some property called 'status' but we will check this later.\r\n                    return response.results[0];\r\n                }));\r\n    }\r\n\r\n    /**\r\n     * Retrieves a connections status from a gatewayUrl status provider\r\n     * @param nodeName the node name.\r\n     * @param relativeUrl the relative url from the relativeGatewayUrl provider.\r\n     */\r\n    private getConnectionStatusFromGatewayUrl(nodeName: string, relativeUrl: string): Observable<ConnectionStatusResult> {\r\n        if (!relativeUrl) {\r\n            // log warning about this condition\r\n            const message = this.connectionStrings.ErrorGatewayUrl.message;\r\n            Logging.logError('ConnectionStream', message);\r\n            return throwError(() => new Error(message));\r\n        }\r\n\r\n        relativeUrl = relativeUrl.replace('$connectionName', nodeName);\r\n\r\n        // scan GWv2 API and use nodeConnection API so it can include authorization information.\r\n        // /Services/LinuxBase/ssh/nodes/$connectionName/connectionstatus\r\n        const segments = MsftSme.trimStart(relativeUrl, '/').split('/');\r\n        if (segments.length > 4 && segments[0].localeCompareIgnoreCase('services') === 0) {\r\n            segments.shift();\r\n            const serviceName = segments.shift();\r\n            const controllerName = segments.shift();\r\n            segments.shift();\r\n            segments.shift();\r\n            const remained = segments.join('/');\r\n            return this.nodeConnection.get({ serviceName, controllerName,  nodeName }, remained, <NodeRequest>{ noAuth: true });\r\n        }\r\n\r\n        return this.gatewayConnection.get(relativeUrl);\r\n    }\r\n\r\n    /**\r\n     * Retrieves a connections status from a extension service method\r\n     */\r\n    private getConnectionStatusFromService(nodeName: string, methodId: ExtensionMethodIdentifier): Observable<ConnectionStatusResult> {\r\n        if (!methodId) {\r\n            // log warning about this condition\r\n            const message = this.connectionStrings.ErrorService.message;\r\n            Logging.log({\r\n                source: 'ConnectionStream',\r\n                level: LogLevel.Error,\r\n                message: message\r\n            });\r\n            return throwError(() => new Error(message));\r\n        }\r\n\r\n        const entryPointId = EnvironmentModule.createEntrypointId(methodId.module, methodId.name);\r\n        return this.extensionBroker.callService(entryPointId, methodId.method, methodId.version, nodeName);\r\n    }\r\n\r\n    /**\r\n     * Retrieves a connections status from a extension worker method\r\n     */\r\n    private getConnectionStatusFromWorker(nodeName: string, methodId: ExtensionMethodIdentifier): Observable<ConnectionStatusResult> {\r\n        if (!methodId) {\r\n            // log warning about this condition\r\n            const message = this.connectionStrings.ErrorWorker.message;\r\n            Logging.log({\r\n                source: 'ConnectionStream',\r\n                level: LogLevel.Error,\r\n                message: message\r\n            });\r\n            return throwError(() => new Error(message));\r\n        }\r\n\r\n        const entryPointId = EnvironmentModule.createEntrypointId(methodId.module, methodId.name);\r\n        return this.extensionBroker.runWorker(entryPointId, methodId.method, methodId.version, nodeName);\r\n    }\r\n}\r\n"]}