@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
628 lines (626 loc) • 24.4 kB
JavaScript
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