UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

749 lines (747 loc) 27.6 kB
import { of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { Crypto } from './crypto'; /** * Net communication class. * @dynamic */ export class Net { static cachedErrorCodeMap; static get errorCodeMap() { if (!Net.cachedErrorCodeMap) { const strings = MsftSme.getStrings().MsftSmeShell.Core; Net.cachedErrorCodeMap = { // added from https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx 0: strings.ErrorCode.Code0.message, 5: strings.ErrorCode.Code5.message, 50: strings.ErrorCode.Code50.message, 87: strings.ErrorCode.Code87.message, 110: strings.ErrorCode.Code110.message, 1323: strings.ErrorCode.Code1323.message, 1326: strings.ErrorCode.Code1326.message, 1355: strings.ErrorCode.Code1355.message, 2224: strings.ErrorCode.Code2224.message, 2691: strings.ErrorCode.Code2691.message, 2692: strings.ErrorCode.Code2692.message, 0x80041087: strings.ErrorCode.Code8004108.message }; } return Net.cachedErrorCodeMap; } /** * The static definition of Web API URLs. */ static apiVersionParam = 'api-version'; static apiVersion20190201 = '2019-02-01'; static apiRoot = '/api/{0}'; static batch = '/batch'; static streamSocket = '{0}/api/streams/socket/{1}'; static downlevel = 'features/downlevelSupport'; static downlevelInstall = Net.downlevel + '/install'; static downlevelComponents = Net.downlevel + '/components'; static extensionsSettings = '/settings/extension'; static installedExtensions = '/settings/extension/installed'; static extensions = '/extensions'; static updatesSettings = '/settings/updates'; static isExtensionUpdateAvailable = Net.extensions + '/isUpdateAvailable'; static cimClass = 'features/cim/namespaces/{0}/classes/{1}'; static cimQuery = 'features/cim/query'; static cimInvoke = '/methods/{0}/invoke'; static powerShellApiInvokeCommand = 'features/powershellApi/invokeCommand'; static powerShellApiSessions = 'features/powershellApi/pssessions/{0}'; static powerShellApiExecuteCommand = Net.powerShellApiSessions + '/invokeCommand'; static powerShellApiRetrieveOutput = Net.powerShellApiSessions + '?$expand=output'; static powerShellApiCancelCommand = Net.powerShellApiSessions + '/cancel'; static powerShellConsoleSessions = 'features/powershellConsole/pssessions/{0}'; static powerShellConsoleExecuteCommand = Net.powerShellConsoleSessions + '/invokeCommand'; static powerShellConsoleRetrieveOutput = Net.powerShellConsoleSessions + '?$expand=output'; static stopCommand = Net.powerShellConsoleSessions + '/cancel'; static tabCommand = Net.powerShellConsoleSessions + '/tab'; static userProfile = '/settings/user'; static applicationSettings = '/settings/all'; static adminSettings = '/settings/admin'; static user = '/user'; static fileTransferFormat = 'features/fileTransfer/files/{0}'; static fileTransferDownloadPost = Net.fileTransferFormat + '/download'; static fileTransferUpload = Net.fileTransferFormat + '/uploadlink'; static jeaFeature = 'features/jea/endpoint'; static jeaExport = Net.jeaFeature + '/export'; static gateway = '/gateway'; static gatewayAccessCheck = Net.gateway + '/access/check'; static gatewayStatus = Net.gateway + '/status'; // {HttpMethod} {relativeNodeUrl} HTTP/1.1 static multiPartCallBodyUrl = '{0} {1} HTTP/1.1'; /** * Gateway version 2.0.0 Node API set. */ /** * WinREST service on Windows platform. */ static serviceWinRest = 'WinREST'; /** * WinStream service on Windows platform. */ static serviceWinStream = 'WinStream'; /** * LinuxBase on Linux platform. */ static serviceLinuxBase = 'LinuxBase'; /** * ActiveDirectory on WinREST. */ static controllerActiveDirectory = 'ActiveDirectory'; /** * CIM on WinREST. */ static controllerCim = 'CIM'; /** * Extensions on WinREST. */ static controllerExtensions = 'Extensions'; /** * FileTransfer on WinREST. */ static controllerFileTransfer = 'FileTransfer'; /** * JEA on WinREST. */ static controllerJea = 'JEA'; /** * Nuget on WinREST. */ static controllerNuget = 'Nuget'; /** * PerformanceCounter on WinREST. */ static controllerPerformanceCounter = 'PerformanceCounter'; /** * PowerShell on WinREST. */ static controllerPowerShell = 'PowerShell'; /** * PseudoConsole on WinStream. */ static controllerPseudoConsole = 'PseudoConsole'; /** * Ssh on LinuxBase. */ static controllerSsh = 'Ssh'; /** * SshFile on LinuxBase. */ static controllerSshFile = 'SshFile'; /** * State on WinREST. */ static controllerState = 'State'; /** * Stream on WinStream. */ static controllerStream = 'Stream'; /** * Stream on LinuxBase. */ static controllerStreamSsh = 'StreamSsh'; /** * Tcp on WinStream and LinuxBase. */ static controllerTcp = 'Tcp'; /** * Wdac on WinREST. */ static controllerWdac = 'Wdac'; /** * Socket URL on WinStream. */ static streamSocketV200 = '{0}/api/services/' + Net.serviceWinStream + '/' + Net.controllerStream + '/socket/{1}'; /** * Socket URL on LinuxBase. */ static sshStreamSocket = '{0}/api/services/' + Net.serviceLinuxBase + '/' + Net.controllerStreamSsh + '/socket'; /** * Encodes the specified data as Base64-encoded URL. * * If the resultant URL is longer than 260 characters, it is converted to a * relative path where each segment (e.g. '/') defines the character length boundary. * The HTTP.SYS subsystem in Windows does now allow a URL segment larger than 260 * characters without changing a Registry key and restarting the OS. * @param data The data to be encoded. * @return An encoded Base64 URL, potentially segmented by '/' every 260 characters. */ static toSegmentedBase64Url(data) { const MAX_HTTP_SYS_URL_SEGMENT_LENGTH = 260; const base64Url = Net.utf8Base64UrlEncode(data); const parts = Net.splitByLength(base64Url, MAX_HTTP_SYS_URL_SEGMENT_LENGTH); return parts.join('/'); } /** * Update URL with api-version=20190201. * @param url the original URL * @returns new url string added version parameter. */ static updateApiVersion20190201(url) { const index = url.indexOf('?'); const prefix = index > 0 ? '&' : '?'; if (index > 0 && url.indexOf(`${Net.apiVersionParam}=`) > 0) { return url; } return `${url}${prefix}${Net.apiVersionParam}=${Net.apiVersion20190201}`; } /** * Convert IPV6 address to literal format. * * @param ipv6Address the ipv6 address format. */ static convertIPv6ToLiteral(ipv6Address) { let data = ''; for (let i = 0; i < ipv6Address.length; i++) { let ch = ipv6Address.charAt(i); if (ch === ':') { ch = '-'; } else if (ch === '%') { ch = 's'; } data += ch; } return `${data}.ipv6-literal.net`; } /** * Convert IPV6 address to literal format. * * @param ipv6Address the ipv6 address format. */ static convertLiteralToIPv6(literal) { const firstSegment = literal.split('.ipv6-literal.net')[0]; if (!firstSegment) { throw new Error('Not recognized as an IPv6 literal format: \'{0}\'!'.format(literal || 'null')); } let data = ''; for (let i = 0; i < firstSegment.length; i++) { let ch = firstSegment.charAt(i); if (ch === '-') { ch = ':'; } else if (ch === 's') { ch = '%'; } data += ch; } return data; } /** * Encode a string with base64url. * * @param data the input string. * @return string the encoded string. */ static base64urlEncode(data) { // REF: https://tools.ietf.org/html/rfc4648#section-5 const base64 = window.btoa(data); let base64Url = ''; for (let i = 0; i < base64.length; i++) { const ch = base64.charAt(i); switch (ch) { case '+': base64Url += '-'; break; case '/': base64Url += '_'; break; case '=': return base64Url; default: base64Url += ch; break; } } return base64Url; } /** * Decode a base64 url string. * * @param data the string to decode. * @return string the decoded string. */ static base64urlDecode(data) { while (data.length % 4 !== 0) { data += '='; } data = data.replaceAll('-', '+').replaceAll('_', '/'); return window.atob(data); } /** * Encode utf8 string. * * @param data the unencoded string. */ static utf8Encode(data) { return unescape(encodeURIComponent(data)); } /** * Decode utf8 string. * * @param data the encoded UTF8 string. */ static utf8Decode(data) { return decodeURIComponent(escape(data)); } /** * Encode with utf8 (first) and base64url (second). * * @param data data the original string to encode. The string can be full unicode character string. * @return string the encoded string used on a part of URL. */ static utf8Base64UrlEncode(data) { const utf8 = Net.utf8Encode(data); return Net.base64urlEncode(utf8); } /** * Decode with utf8 (second) and base64url (first). * * @param data data the encoded URL string to decode. * @return string the decoded unicode string. */ static utf8Base64UrlDecode(data) { const utf8 = Net.base64urlDecode(data); return Net.utf8Decode(utf8); } /** * Create a key name from key value pairs. * * @param properties the key value pairs. * @return string the key name. */ static cimCreateName(properties) { const data = JSON.stringify(properties); const utf8 = Net.utf8Encode(data); return Net.base64urlEncode(utf8); } /** * Get properties of the item from the response. * * @param data the item in the response object. * @return any the properties. */ static getItemProperties(data) { if (data && data.properties) { return data.properties; } return data; } /** * Get properties of first item from the response. * * @param data the response object. * @return any the properties. */ static getFirstProperties(data) { if (data && data.value && data.value.length) { if (data.value[0].properties) { return data.value[0].properties; } } else if (data && data.length) { return data[0]; } return Net.getItemProperties(data); } /** * Get array of items from the response. * * @param data the response object. * @return any the item array. */ static getItemArray(data) { if (data && data.value) { return data.value; } return data; } /** * Create JSON string with properties. * * @param data the input data. * @return string the JSON string with properties. */ static createPropertiesJSONString(data) { return JSON.stringify({ properties: data }); } /** * Creates an encoded authentication header. * * @param usersName name of user. * @param password the password. * @return the token string. */ static createEncodedAuthenticationHeader(userNames, password, passwordEncryptedWith = null) { const credentials = { ...Net.toUsernameAndDomain(userNames), password: password }; if (!MsftSme.isNullOrWhiteSpace(passwordEncryptedWith)) { credentials.passwordEncryptedWith = passwordEncryptedWith; } return window.btoa(Net.utf8Encode(JSON.stringify(credentials))); } static toUsernameAndDomain(value) { let username; if (Array.isArray(value)) { username = value; } else if (value.indexOf('@') >= 0) { // domain is empty if UPN is used. username = ["", value]; } else { username = value.split('\\'); } return { // if only a username was provided, use '.' (shorthand for the locale machine hostname) domain: username.length === 1 ? '.' : username[0], username: username.length === 1 ? username[0] : username[1] }; } /** * Creates an encrypted authentication header value. * * @param jwk the JWK (Json Web Key) * @param usersName name of user. * @param password the password. * @param logonUser the gateway user * @param expirationTimeInMs manage as token expiration time in milliseconds * @return the token string. */ static createEncryptedAuthenticationHeader(jwk, userNames, password, logonUser, expirationTimeInMs) { const credentials = { ...Net.toUsernameAndDomain(userNames), password: password, passwordEncryptedWith: undefined }; const passwordData = Net.createPasswordData(credentials, logonUser, expirationTimeInMs); return Crypto.encryptRsaSha1(jwk, passwordData) .pipe(map(encryptedPassword => { credentials.password = encryptedPassword; credentials.passwordEncryptedWith = 'JWK2'; return window.btoa(Net.utf8Encode(JSON.stringify(credentials))); }), catchError(() => of(window.btoa(Net.utf8Encode(JSON.stringify(credentials)))))); } static createPasswordData(credentials, logOnUser, expirationTimeInMs) { const millisecondsInHour = 60 * 60 * 1000; const hoursInYear = 365 * 24; const currentTimeStamp = Date.now(); const expirationOffsetInMilliseconds = expirationTimeInMs || (hoursInYear * millisecondsInHour); const passwordData = { p: credentials.password, l: logOnUser, t: currentTimeStamp, e: currentTimeStamp + expirationOffsetInMilliseconds }; return JSON.stringify(passwordData); } /** * Creates encrypted data for auth header * @param jwk the JSON web key to be used to encrypt data * @param data the data to be encrypted * @returns an encrypted string */ static createEncryptedExtensionDataHeader(jwk, data) { return Crypto.encryptRsaSha1(jwk, data) .pipe(map(encryptedData => window.btoa(Net.utf8Encode(encryptedData)))); } /** * Create /api/nodes URL with relativeUrl. * * @param gatewayName The name of gateway. * @param nodeName The name of node. * @param relativeUrl The relative Url. */ static gatewayNodeApi(gatewayName, nodeName, relativeUrl) { if (!relativeUrl) { relativeUrl = ''; } if (!relativeUrl.startsWith('/')) { relativeUrl = '/' + relativeUrl; } if (!nodeName) { const message = MsftSme.getStrings().MsftSmeShell.Core.Error.ArgumentNullError.message; throw new Error(message.format('Net/gatewayNodeApi', 'nodeName')); } return Net.gatewayApi(gatewayName, `/nodes/${nodeName}${relativeUrl}`); } /** * Create /api URL with relativeUrl. * * @param gatewayName The name of gateway. * @param nodeName The name of node. * @param relativeUrl The relative Url. */ static gatewayApi(gatewayName, relativeUrl) { if (!gatewayName) { const message = MsftSme.getStrings().MsftSmeShell.Core.Error.ArgumentNullError.message; throw new Error(message.format('Net/gatewayApi', 'gatewayName')); } if (!relativeUrl) { relativeUrl = ''; } if (!relativeUrl.startsWith('/')) { relativeUrl = '/' + relativeUrl; } gatewayName = gatewayName.toLowerCase(); if (!gatewayName.startsWith('http://') && !gatewayName.startsWith('https://')) { gatewayName = 'https://' + gatewayName; } return `${gatewayName}/api${relativeUrl}`; } /** * Get error message from ajax result or any other error result and optionally includes native error message. * * @param error the error context from Net.ajax. * @param options add additional optional error message: such as native error messages if possible * @return string the error message. */ static getErrorMessage(error, options) { const strings = MsftSme.getStrings().MsftSmeShell.Core; const prefixFormat = strings.Error.PrefixFormat.message; const errorPrefix = options && options.errorPrefix; const xhr = error && error.xhr; const exception = error && error.exception; if (typeof error === 'string') { return errorPrefix ? prefixFormat.format(error) : error; } if (xhr && xhr.response) { const message = Net.parseErrorResponse(xhr.response, options); if (message) { return errorPrefix ? prefixFormat.format(message) : message; } } if (error && error.message) { const errorMessage = errorPrefix ? prefixFormat.format(error.message) : error.message; if (error.details && Array.isArray(error.details)) { // Add details to error message. return this.parseErrorDetails(error.details, errorMessage); } return errorMessage; } if (exception && exception.message) { return errorPrefix ? prefixFormat.format(exception.message) : exception.message; } const statusText = xhr && xhr.statusText; if (statusText) { return errorPrefix ? prefixFormat.format(statusText) : statusText; } throw new Error(strings.Error.NoResponseError.message); } static parseErrorDetails(details, message) { const strings = MsftSme.getStrings().MsftSmeShell.Core; const detailsText = strings.Error.Details.text; const errorMessages = details.map((detail) => { try { const parsedDetail = JSON.parse(detail.message); if (parsedDetail?.error?.message) { return parsedDetail.error.message; } } catch (error) { // The message is not valid JSON return detail.message; } }); // Joining the error messages with newline characters const detailsMessage = `${detailsText}\n${errorMessages.join('\n\n')}`; // Joining the actual error message with details return `${message}\n\n${detailsMessage}`; } /** * Get error message from PowerShell ajax response. * Can be used by a PowerShell batch consumer to get error message in batch response. * * @param response the ajax response. * @return string the error message. */ static getPowerShellErrorMessage(response) { const message = Net.parseErrorResponse(response); if (message) { return message; } const strings = MsftSme.getStrings().MsftSmeShell.Core; throw new Error(strings.Error.NoResponseError.message); } /** * Get error code from ajax result. * * @param error the error context from Net.ajax. * @return string the error code. */ static getErrorCode(error) { const strings = MsftSme.getStrings().MsftSmeShell.Core; const err = error && error.xhr && error.xhr.response && error.xhr.response.error; if (!err) { throw new Error(strings.Error.NoResponseError.message); } if (err.code) { return err.code; } throw new Error(strings.Error.NoCode.message); } /** * Get error message from ajax result excluding error stackTrace * * @param error the error context from Net.ajax. * @return string the error. */ static getErrorMessageWithoutStacktrace(error) { let errorMessage = Net.getErrorMessage(error); if (errorMessage) { const stackTraceIndex = errorMessage.toLowerCase().indexOf('stacktrace'); if (stackTraceIndex > 0) { errorMessage = errorMessage.substring(0, stackTraceIndex); } } return errorMessage; } /** * Translates error code to string * * @param code the error code * @return string the related error string. */ static translateErrorCode(code) { const strings = MsftSme.getStrings().MsftSmeShell.Core; const message = Net.errorCodeMap[code]; if (message) { return strings.ErrorCode.Translated.message.format(message, code); } return strings.ErrorCode.Generic.message.format(code); } /** * Determine if this is an authorization login error. This code never work if it uses NTLM or Kerberos. * * @param error The ajax error object. */ static isUnauthorizedLogin(error) { // new login 401 handling. const forbidden = error?.xhr?.response?.error?.forbidden; return error.status === 401 /* HttpStatusCode.Unauthorized */ && forbidden && forbidden === 'UnauthorizedLogin'; } /** * Determine if this is an authorization error. * * @param error The ajax error object. */ static isUnauthorized(error) { const errorObject = error?.xhr?.response?.error; const forbidden = errorObject?.forbidden; if (forbidden) { // new 403 handling. return error.status === 403 /* HttpStatusCode.Forbidden */ && forbidden === 'Unauthorized'; } else { // legacy 401 handling. (this should be obsolete now) const unauthorized = errorObject?.unauthorized; return error.status === 401 /* HttpStatusCode.Unauthorized */ && !(unauthorized && unauthorized === 'UnauthorizedLogin'); } } /** * Determine if this is an forbidden error. * * @param error The ajax error object. */ static isForbidden(error) { const forbidden = error?.xhr?.response?.error?.forbidden; if (forbidden) { // new forbidden. return error.status === 400 /* HttpStatusCode.BadRequest */ && forbidden === 'Forbidden'; } else { // legacy 403 handling. return error.status === 403 /* HttpStatusCode.Forbidden */; } } /** * Get property from an ErrorExtended error object * * @param error The ErrorExtended error object. * @param sourceName The source of the error. * @param propertyName The property to get from the error object. * @return The value of the property or null if the source doesn't match. */ static getErrorExtendedProperty(error, sourceName, propertyName) { if (error.extendedSource && (error.extendedSource === sourceName || error.extendedSource.includes(sourceName))) { return error?.extended[propertyName]; } return null; } /** * Parse error message from standard ajax error and PowerShell errors. * * @param response the ajax response. * @return string the error message. */ static parseErrorResponse(response, options) { const strings = MsftSme.getStrings().MsftSmeShell.Core; const err = response && response.error; if (err && err.message) { const errorMessage = err.message; if (options && options.addNativeError && err.detailRecord) { return strings.Error.AddNativeErrorCode.message.format(errorMessage, err.detailRecord.nativeErrorCode); } return errorMessage; } const psErrors = response && response.errors; if (psErrors && psErrors.length > 0) { if (psErrors.length === 1) { if (options && options.addNativeError && psErrors[0].detailRecord) { if (options && options.useRemoteExceptionMessage) { return strings.ErrorFormat.Single.Details.message.format(psErrors[0].errorType, psErrors[0].detailRecord.remoteExceptionMessage, psErrors[0].detailRecord.nativeErrorCode); } return strings.ErrorFormat.Single.Details.message.format(psErrors[0].errorType, psErrors[0].message, psErrors[0].detailRecord.nativeErrorCode); } if (options && options.useRemoteExceptionMessage && psErrors[0].detailRecord) { return strings.ErrorFormat.Single.message.format(psErrors[0].errorType, psErrors[0].detailRecord.remoteExceptionMessage); } return strings.ErrorFormat.Single.message.format(psErrors[0].errorType, psErrors[0].message); } let joinedMessage = ''; for (let i = 0; i < psErrors.length; i++) { if (options && options.addNativeError && psErrors[i].detailRecord) { joinedMessage += strings.ErrorFormat.Multiple.Details.message.format(i + 1, psErrors[i].errorType, psErrors[i].message, psErrors[i].detailRecord.nativeErrorCode); } else if (options && options.useRemoteExceptionMessage && psErrors[i].detailRecord) { joinedMessage += strings.ErrorFormat.Multiple.message.format(i + 1, psErrors[i].errorType, psErrors[i].detailRecord.remoteExceptionMessage); } else { joinedMessage += strings.ErrorFormat.Multiple.message.format(i + 1, psErrors[i].errorType, psErrors[i].message); } } return joinedMessage; } if (response && response.exception) { return response.exception; } return null; } static splitByLength(text, length) { const parts = new Array(); if (text === null) { return parts; } const totalLength = text.length; const count = Math.ceil(totalLength / length); if (count < 2) { parts.push(text); return parts; } const lastPart = count - 1; let position = 0; for (let i = 0; i < lastPart; i++, position += length) { parts.push(text.substring(position, length)); } parts.push(text.substring(position)); return parts; } } //# sourceMappingURL=net.js.map