UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

628 lines (626 loc) 24.4 kB
import { Observable, of, ReplaySubject, throwError } from 'rxjs'; import { catchError, expand, filter, map, mergeMap, switchMap, take, tap } from 'rxjs/operators'; import { EnvironmentModule } from '../manifest/environment-modules'; import { RpcObservableElevateClient } from '../rpc/elevate/rpc-observable-elevate-client'; import { RpcObservableElevateServer } from '../rpc/elevate/rpc-observable-elevate-server'; import { RpcServiceForwarder } from '../rpc/rpc-forwarder'; import { CoreEnvironment } from './core-environment'; import { GatewayUrlBuilder } from './gateway-url-builder'; import { GatewayUrls } from './gateway-urls'; import { Http, HttpMethod } from './http'; import { headerConstants } from './http-constants'; import { Net } from './net'; /** * The Gateway Connection class for creating requests and calling the Gateway's REST API */ export class GatewayConnection extends RpcServiceForwarder { http; authorizationManager; static rpcCommands = { forbiddenReceived: 'forbiddenRecieved' }; /** * The IPv4 address for localhost */ localhostIpV4 = '127.0.0.1'; /** * The IPv6 address for localhost */ localhostIpV6 = '::1'; /** * The localhost name */ localhost = 'localhost'; /** * Time interval to check for internalGatewayStatus refresh. */ lastUpdatedInternalGatewayStatusInterval = 1000 * 60 * 10; // 10 minutes /** * internally maintained gateway URL. */ internalGatewayUrl; /** * internally maintained static version. */ internalStaticVersion; /** * gatewayUrl param is specified. */ internalGatewayUrlSpecified; /** * Last time internal gateway status was updated in milliseconds */ lastUpdatedInternalGatewayStatus; /** * Internally maintained gateway status. */ internalGatewayStatus; /** * The replay subject for gateway url to settle. */ gatewayUrlAwaiter = new ReplaySubject(1); /** * The RPC observable elevate client. */ elevateClient = null; /** * The RPC observable elevate server. */ elevateServer = null; /** * The elevate callback observable. */ elevateCallbackInternal = () => of(false); /** * The replay subject for navigation ready state. */ navigationReady = new ReplaySubject(1); /** * Sets the elevate callback observable. */ set elevateCallback(value) { this.elevateCallbackInternal = value; if (!this.elevateServer) { this.elevateServer = new RpcObservableElevateServer(this.rpc); this.elevateServer.register((_context) => { return this.elevateCallbackInternal() .pipe(catchError((error, _caught) => throwError(() => ({ error: Net.getErrorMessage(error) }))), map(result => ({ elevated: result }))); }); } } /** * Indicates that the gateway is disabled and therefore should not be called. */ get disabled() { const environment = MsftSme.self().Environment; return environment && environment.configuration.gateway.disabled; } /** * Gets the gateway URL to connect to. */ get gatewayUrl() { return this.internalGatewayUrl; } /** * Gets the gateway URL specified state. */ get staticVersion() { return this.internalStaticVersion; } /** * Gets the gateway URL specified state. */ get gatewayUrlSpecified() { return this.internalGatewayUrlSpecified; } /** * Gets the gateway URL observable while setting up. */ get gatewayUrlObservable() { return this.gatewayUrlAwaiter.asObservable(); } /** * Gets the navigation ready observable. */ get navigationReadyObservable() { return this.navigationReady.asObservable(); } /** * Sets the navigation ready status. */ set navigationReadyStatus(ready) { this.navigationReady.next(ready); } /** * Gets the gateway information. */ get gatewayInfo() { if (!this.internalGatewayUrl) { throw new Error(MsftSme.getStrings().MsftSmeShell.Core.Error.GatewayUrlNotConfigured.message); } // RegEx: ('http' or 'https') '://' (('<gatewayName1>'):('<port>') or ('<gatewayName2>')) // 0: url // 1: https or http // 2: <gatewayName1>:<port> or <gatewayName2> // 3: <gatewayName1> or undefined // 4: <port> or undefined // 5: <gatewayName2> const url = MsftSme.trimEnd(this.internalGatewayUrl.toLowerCase(), '/'); const match = url.match(/(http|https):\/\/((.+):(\d+)|(.+))/); if (!match) { throw new Error(MsftSme.getStrings().MsftSmeShell.Core.Error.GatewayUrlMalformed.message); } const secure = (match[1] === 'https'); const name = match[3] || match[2]; const port = parseInt(match[4], 10) || (secure ? 443 : 80); return { name, secure, port }; } /** * Gets the gateway node name to make a CIM/PowerShell query to the gateway node. */ get gatewayName() { // localhost will be used to locally query gateway node. if this causes any access problem, need to be replaced. return 'localhost'; } /** * Gets gateway status from cache or sets cache if it doesn't exist. */ get gatewayStatus() { if (!this.internalGatewayStatus || Date.now() - this.lastUpdatedInternalGatewayStatus > this.lastUpdatedInternalGatewayStatusInterval) { return this.getStatus(); } return of(this.internalGatewayStatus); } /** * Initializes a new instance of the Gateway class. * * @param http the Http object. * @param rpc the Rpc class. * @patam authorizationManager the authorization manager class object. */ constructor(http, rpc, authorizationManager) { super('gateway-connection', rpc); this.http = http; this.authorizationManager = authorizationManager; // restore the AAD token if exist becase ealier access of token is required for gateway calls. authorizationManager.signOnManager.applySignedHttpRequestToken(MsftSme.self().Environment.configuration?.signOn?.signedHttpRequestToken); } /** * Configure the gateway URL to connect to. */ configureGatewayEnvironment(url, urlSpecified, staticVersion) { // trim last "/" if (url[url.length - 1] === '/') { url = url.substring(0, url.length - 1); } this.internalGatewayUrl = url; this.internalGatewayUrlSpecified = urlSpecified, this.internalStaticVersion = staticVersion; this.gatewayUrlAwaiter.next(url); } /** * Update the url with static version option. * @param url the original URL including options. * @returns updated url with "version=" option if staticVersion present. */ addStaticVersion(url) { if (MsftSme.isNullOrWhiteSpace(this.staticVersion) || this.staticVersion.indexOf('staticVersion=') < 0) { return url; } const splitter = url.indexOf('?') > 0 ? '&' : '?'; return `${url}${splitter}staticVersion=${this.staticVersion}`; } /** * Makes a POST call to the gateway * * @param relativeUrl the relative Url after "/api" * @param body the body string JSON.stringfy'ed * @param request the gateway request object. */ post(relativeUrl, body, request) { const postRequest = this.createRequest(HttpMethod.Post, relativeUrl, body, request); return this.call(postRequest); } /** * Makes a GET call to the gateway * * @param relativeUrl the relative Url after "/api" * @param request the gateway request object. */ get(relativeUrl, request) { const getRequest = this.createRequest(HttpMethod.Get, relativeUrl, null, request); return this.call(getRequest); } /** * Makes a PUT call to the gateway * * @param relativeUrl the relative Url after "/api" * @param body the body string JSON.stringfy'ed * @param request the gateway request object. */ put(relativeUrl, body, request) { const putRequest = this.createRequest(HttpMethod.Put, relativeUrl, body, request); return this.call(putRequest); } /** * Makes a PATCH call to the gateway * * @param relativeUrl the relative Url after "/api" * @param body the body string JSON.stringfy'ed * @param request the gateway request object. */ patch(relativeUrl, body, request) { const patchRequest = this.createRequest(HttpMethod.Patch, relativeUrl, body, request); return this.call(patchRequest); } /** * Makes a DELETE call to the gateway * * @param relativeUrl the relative Url after "/api" * @param body the body string JSON.stringfy'ed * @param request the gateway request object. */ delete(relativeUrl, body, request) { const deleteRequest = this.createRequest(HttpMethod.Delete, relativeUrl, body, request); return this.call(deleteRequest); } /** * Makes a DELETE call to the gateway without waiting for the response. * * @param relativeUrl the relative Url after "/api" * @param request the gateway request object. */ deleteQuick(relativeUrl, headers) { headers[headerConstants.MODULE_NAME] = EnvironmentModule.getModuleName(); headers[headerConstants.MODULE_VERSION] = EnvironmentModule.getModuleVersion(); headers[headerConstants.ACCEPT_LANGUAGE] = CoreEnvironment.localizationManager.getLocaleId().neutral; if (this.staticVersion) { headers[headerConstants.STATIC_VERSION] = this.staticVersion; } if (this.authorizationManager.signOnManager.isSignOnTokenEnabled) { this.authorizationManager.signOnManager.SetAadAuthorizationHeader(headers); } const url = Net.gatewayApi(this.gatewayUrl, relativeUrl); this.http.deleteQuick(url, headers); } /** * Creates a GatewayRequest. * * @param method the http method to use * @param relativeUrl the relative Url after "/api/" * @param body the body string JSON.stringfy'ed * @param request the gateway request object to extend. */ createRequest(method, relativeUrl, body, request) { const defaultMaxRetry = 3; request = MsftSme.deepAssign({}, request); // if request is undefined, default to empty object request = request || {}; request.headers = request.headers || {}; request.headers[headerConstants.MODULE_NAME] = EnvironmentModule.getModuleName(); request.headers[headerConstants.MODULE_VERSION] = EnvironmentModule.getModuleVersion(); request.headers[headerConstants.ACCEPT_LANGUAGE] = CoreEnvironment.localizationManager.getLocaleId().neutral; if (this.staticVersion) { request.headers[headerConstants.STATIC_VERSION] = this.staticVersion; } // use default retry options if none are provided const retryHandlers = []; if (this.authorizationManager.signOnManager.isSignOnTokenEnabled) { this.authorizationManager.signOnManager.SetAadAuthorizationHeader(request.headers); retryHandlers.push({ canHandle: (code, error) => this.authorizationManager.signOnManager.canHandleUnauthorizedLogin(code, error), handle: (code, originalRequest, error) => this.authorizationManager.signOnManager.handleUnauthorizedLogin(code, originalRequest, error) }); } if (request.retryHandlers != null && request.retryHandlers.length > 0) { retryHandlers.push(...request.retryHandlers); } return Object.assign(request, { method: method, url: relativeUrl, // default to the passed in body, the request body, or an empty string body: body || request.body || '', // default to the request headers, or an empty object headers: request.headers, // for the next 2 props, default to true unless explicitly set to false withCredentials: request.withCredentials === false ? false : true, crossDomain: request.crossDomain === false ? false : true, createXHR: () => new XMLHttpRequest(), retryHandlers, maxRetryCount: request.maxRetryCount === 0 ? 0 : request.maxRetryCount || defaultMaxRetry }); } /** * Make a request. * * @param request the request to execute against the gateway. * @return Observable<any> the query result observable. */ call(request) { if (!this.gatewayUrl) { return this.gatewayUrlAwaiter.pipe(mergeMap(() => this.call(request))); } // create gateway url from current url if not set yet. if (!request.url || (!request.url.startsWith('http://') && !request.url.startsWith('https://'))) { request.url = Net.gatewayApi(this.gatewayUrl, request.url); } // create retry options from request const retryOptions = { handlers: (request.retryHandlers || []), maxRetry: request.maxRetryCount }; // create observable for our request const requestObservable = this.http.request(request, retryOptions) .pipe(map((response) => response ? response.response : {})); if (request.beforeCall) { return request.beforeCall(request).pipe(mergeMap(() => requestObservable)); } return requestObservable; } /** * Gets default secure request options. * * @returns updated request object. */ get defaultHttpSecureOptions() { const request = { ...{ headers: {} }, ...Http.defaultHttpOptions }; if (this.authorizationManager.signOnManager.isSignOnTokenEnabled) { this.authorizationManager.signOnManager.SetAadAuthorizationHeader(request.headers); } return request; } /** * Check if elevation is required from the error object. * * @param error the ajax error object. */ isElevationRequired(error) { const code = error.xhr && error.xhr.response && error.xhr.response.error && error.xhr.response.error.code; return code === 'ElevationRequired'; } /** * Elevate the gateway if it's desktop mode running. */ elevate() { if (!this.elevateClient) { this.elevateClient = new RpcObservableElevateClient(this.rpc); } return this.elevateClient.elevate().pipe(map(result => result.elevated)); } /** * Creates and returns a new URL builder for the current connection. */ url() { return new GatewayUrlBuilder(this.gatewayUrl); } /** * Clear the DNS cache. */ clearDnsCache() { const dnsCacheClearUrl = 'gateway/dnsCacheClear'; return this.delete(dnsCacheClearUrl) .pipe(map(_ => null)); } /** * Polling to check if the gateway is elevated. */ pollingGatewayElevated() { // user might not respond for elevation prompt, maximum waiting time is 2.5 min. // getElevatedStatus() takes 5000ms when the gateway is not responding. // 2.5 min = count [30] * 5000 msec. const uniQueId = MsftSme.getUniqueId(); const maxWaitingCount = 30; let count = maxWaitingCount; return this.getElevatedStatus() .pipe(expand(status => { // user accepted. // adds 5 calls for the gateway dead period. // < Restart Requested from UI > // 1st query: it shows success on slow performed system, the gateway is even not shutdown yet. // 2nd query: It could success still. // 3rd query: Maybe timeout because the gateway is not started yet. // 4rd query: Maybe timeout because the gateway is not started yet. // 5rd query: Maybe timeout because the gateway is down. // 25 = 30 - 5 const pollingStartCount = 25; if (((count <= pollingStartCount && (!status.error || status.isGatewayProcessElevated)) || status.completed) && (status.id === uniQueId)) { return of({ completed: true, id: uniQueId, isGatewayProcessElevated: status.isGatewayProcessElevated }); } // no response for more than 2.5 minutes. // gateway was stopped for long or user never responded to UAC prompt. if (count-- <= 0 && status.id === uniQueId) { return of({ completed: true, id: uniQueId, isGatewayProcessElevated: status.isGatewayProcessElevated }); } return this.getElevatedStatus(); }), filter(result => result.isGatewayProcessElevated || result.completed), take(1), map(result => result.isGatewayProcessElevated)); } /** * Perform gateway status query but cut off if it exceeds 2.5 seconds. */ getElevatedStatus() { // if the gateway is not responding for 5000ms, cancel the call. const callCancelTime = 5000; return new Observable(observer => { let subscription = null; let timer = setTimeout(() => { // handle pending timeout of the query when the gateway doesn't respond. if (subscription) { observer.next({ error: true, isGatewayProcessElevated: false, completed: undefined, id: undefined }); observer.complete(); subscription.unsubscribe(); subscription = null; timer = null; } }, callCancelTime); subscription = this.getStatus() .subscribe({ next: data => { observer.next({ error: false, isGatewayProcessElevated: data.isGatewayProcessElevated, completed: undefined, id: undefined }); observer.complete(); subscription.unsubscribe(); subscription = null; if (timer) { clearTimeout(timer); } }, error: () => { observer.next({ error: true, isGatewayProcessElevated: false, completed: undefined, id: undefined }); observer.complete(); subscription.unsubscribe(); subscription = null; if (timer) { clearTimeout(timer); } } }); }); } /** * Gets gateway machine if found in list of given nodes * @param nodes Node names or IPs to check * @returns Node name of gateway if found, null otherwise */ getGateway(nodes) { return this.gatewayStatus.pipe(map(status => { for (const node of nodes) { if (MsftSme.isNullOrUndefined(node)) { continue; } switch (node.toLocaleLowerCase()) { case this.localhostIpV4: case this.localhostIpV6: case this.localhost: case status.fullyQualifiedDNSName?.toLocaleLowerCase(): case status.name?.toLocaleLowerCase(): case status.machineName?.toLocaleLowerCase(): return node; } if (status.addressList?.find((address) => address === node)) { return node; } } return null; })); } /** * Check gateway condition. */ checkCondition() { const result = {}; return this.getStatus() .pipe(switchMap(gateway => { if (gateway.gatewayMode === 'Service') { result.isServiceMode = true; result.isGatewayProcessElevated = false; return this.getAccessCheck(); } result.isServiceMode = false; result.isGatewayProcessElevated = gateway.isGatewayProcessElevated; return of(true); }), map((isAdmin) => { result.isGatewayAdmin = !!isAdmin; return result; }), take(1)); } /** * Get gateway status. */ getStatus() { return this.get('gateway/status').pipe(tap(status => { this.lastUpdatedInternalGatewayStatus = Date.now(); this.internalGatewayStatus = status; })); } /** * Get user access check. */ getAccessCheck() { return this.get(GatewayUrls.accessCheck); } /** * Called on a child service instance when onForwardInit returns from the parent * Initialize telemetry from within here so we can leverage gateway status for telemetry metadata. * @param data The response from the forwardInit call */ onForwardInitResponse(data) { if (data.error) { // if there is an error, we cannot continue, so throw its throw data.error; } if (!MsftSme.self().Environment.configuration.gateway.disabled) { this.internalGatewayUrl = data.result.gatewayName; this.internalGatewayUrlSpecified = data.result.gatewayUrlSpecified; this.internalStaticVersion = data.result.staticVersion; } } /** * Called when a new instance of the service in another window is initialized and needs to synchronize with its parent * @param from The RpcRelationshipType that this request is from * @returns an observable for the all the values needed to initialize the service */ onForwardInit() { if (this.gatewayUrl) { return of({ gatewayName: this.gatewayUrl, gatewayUrlSpecified: this.gatewayUrlSpecified, staticVersion: this.staticVersion }); } else { // if gateway value hasn't been set yet, then wait for it. if (MsftSme.self().Environment.configuration.gateway.disabled) { return of(null); } return this.gatewayUrlAwaiter .pipe(map(url => ({ gatewayName: url, gatewayUrlSpecified: this.gatewayUrlSpecified, staticVersion: this.staticVersion }))); } } /** * Called when the forwarded services counterpart wants to get data from the parent * @param from The RpcRelationshipType that this request is from * @param name The name of the method to forward to * @param args The arguments of the method * @returns an observable for the result of the method call */ onForwardExecute(from, name) { // gatewayConnection does not allow any method calls at this time return this.nameNotFound(name); } /** * Called when the forwarded services counterpart sends a notify message * @param from The RpcRelationshipType that this request is from * @param name The name of the property to change * @param value The new value of the property * @returns an observable that completes when the property has been changed. */ onForwardNotify(from, name) { if (from === 1 /* RpcRelationshipType.Child */ && name === GatewayConnection.rpcCommands.forbiddenReceived) { // Deprecated noop version 1.1100.0 at 10/1/2020. Using ErrorMonitor. return of(null); } // gatewayConnection does not allow any other notifications at this time return this.nameNotFound(name); } } //# sourceMappingURL=gateway-connection.js.map