UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

535 lines (533 loc) 26.9 kB
import { map } from 'rxjs/operators'; import { LogLevel } from '../diagnostics/log-level'; import { Logging } from '../diagnostics/logging'; import { EnvironmentModule } from '../manifest/environment-modules'; import { HttpMethod } from './http'; import { headerConstants } from './http-constants'; import { Net } from './net'; /** * The Batch Connection class for creating requests and calling the Gateway's Http API */ export class BatchConnection { gateway; authorizationManager; moduleName = null; /** * Initializes a new instance of the BatchConnection class. * * @param gateway the gateway Connection * @param authorizationManager the authorization manager. */ constructor(gateway, authorizationManager) { this.gateway = gateway; this.authorizationManager = authorizationManager; } /** * Makes a batch call to the gateway api, by using provided methods for each node. * * Auth handling: createBody() handles the Auth for individual calls in the batch. It gets the needed tokens * for each node and adds to the batch body, along with rest of node call. * See authorizationManager.addAuthorizationTokensToMultiPartBody() for details. * For the outer batch call, we use the auth token from the first node in list. * @param nodesList : list of nodes we will be running the batch against. * @param relativeUrlList : list of relative Urls of nodes after "/api/nodes/<nodeName>/" * @param bodyCommandList : list of body commands, that will be present in body of a typical Post call. * This is essentially a json request that we keep as a string to put it in the body. * Ex: { * "properties": { * "command": "##GetVirtualMachines##\n\nSet-StrictMode -Version 5.0\nget-vm | Select-Object name, id, CPUUsage" * } * } * * @param methodsList : list of Http methods, with each item corresponding to the node in nodeList and relativeUrl in relativeUrlList. * @param request : optional request Properties. */ mixed(nodesList, relativeUrlList, bodyCommandList, methodsList, request) { const batchCallRelativeUrl = Net.updateApiVersion20190201(Net.batch); const boundary = MsftSme.newGuid(); const guidsList = this.generateGuidsList(nodesList.length); const guidsToRequestCtxMap = this.generateGuidToRequestContextMap(nodesList, guidsList); // populate request properties. request = this.createBatchRequest(request || {}); // populate batch request headers. this.setRequestHeaders(request, boundary); // Create batch command body. const body = this.createBody(nodesList, relativeUrlList, guidsList, methodsList, boundary, bodyCommandList, request); return this.gateway.post(batchCallRelativeUrl, body, request) .pipe(map((responseData) => { const parsedResponse = this.parseMultiPartResponse(responseData, guidsToRequestCtxMap); return parsedResponse; })); } /** * Makes a batch POST calls to the gateway api * * Auth handling: createBody() handles the Auth for individual calls in the batch. It gets the needed tokens * for each node and adds to the batch body, along with rest of node call. * See authorizationManager.addAuthorizationTokensToMultiPartBody() for details. * For the outer batch call, we use the auth token from the first node in list. * @param nodesList : list of nodes we will be running the batch against. * @param relativeUrlList : list of relative urls after "/api/nodes/<nodeName>/", for each node. * @param bodyCommandList : list of body commands, that will be present in body of a typical Post call. * @param request : optional request Properties. */ post(nodesList, relativeUrlList, bodyCommandList, request) { const batchCallRelativeUrl = Net.updateApiVersion20190201(Net.batch); const boundary = MsftSme.newGuid(); const guidsList = this.generateGuidsList(nodesList.length); const guidsToRequestCtxMap = this.generateGuidToRequestContextMap(nodesList, guidsList); // populate request properties. request = this.createBatchRequest(request || {}); // populate batch request headers. this.setRequestHeaders(request, boundary); // Create batch command body. const body = this.createBodySingleMethod(nodesList, relativeUrlList, guidsList, HttpMethod.Post, boundary, bodyCommandList, request); return this.gateway.post(batchCallRelativeUrl, body, request) .pipe(map((responseData) => { const parsedResponse = this.parseMultiPartResponse(responseData, guidsToRequestCtxMap); return parsedResponse; })); } /** * Makes a batch GET call to the gateway api * * @param nodeList: the list of names of the node to call the API for. * @param relativeUrlList: the list of relative Url after "/api/nodes/<nodeName>/" * @param request: the batch request object. */ get(nodesList, relativeUrlList, request) { const batchCallRelativeUrl = Net.updateApiVersion20190201(Net.batch); const boundary = MsftSme.newGuid(); const guidsList = this.generateGuidsList(nodesList.length); const guidsToRequestCtxMap = this.generateGuidToRequestContextMap(nodesList, guidsList); // populate request properties. request = this.createBatchRequest(request || {}); // populate batch request headers. this.setRequestHeaders(request, boundary); // Create batch command body. const body = this.createBodySingleMethod(nodesList, relativeUrlList, guidsList, HttpMethod.Get, boundary, null, request); return this.gateway.post(batchCallRelativeUrl, body, request) .pipe(map((responseData) => { const parsedResponse = this.parseMultiPartResponse(responseData, guidsToRequestCtxMap); return parsedResponse; })); } /** * Makes a batch PUT call to the gateway api * * @param nodesList : list of nodes we will be running the batch against. * @param relativeUrlList : list of relative Urls of nodes after "/api/nodes/<nodeName>/" * @param bodyCommandList : list of body commands, that will be present in body of a typical Post call. * This is essentially a json request that we keep as a string to put it in the body. * Ex: { * "properties": { * "command": "##GetVirtualMachines##\n\nSet-StrictMode -Version 5.0\nget-vm | Select-Object name, id, CPUUsage" * } * } * * @param request : optional request Properties. */ put(nodesList, relativeUrlList, bodyCommandList, request) { const batchCallRelativeUrl = Net.updateApiVersion20190201(Net.batch); const boundary = MsftSme.newGuid(); const guidsList = this.generateGuidsList(nodesList.length); const guidsToRequestCtxMap = this.generateGuidToRequestContextMap(nodesList, guidsList); // populate request properties. request = this.createBatchRequest(request || {}); // populate batch request headers. this.setRequestHeaders(request, boundary); // Create batch command body. const body = this.createBodySingleMethod(nodesList, relativeUrlList, guidsList, HttpMethod.Put, boundary, bodyCommandList, request); return this.gateway.post(batchCallRelativeUrl, body, request) .pipe(map((responseData) => { const parsedResponse = this.parseMultiPartResponse(responseData, guidsToRequestCtxMap); return parsedResponse; })); } /** * Makes a batch DELETE call to the gateway api * * @param nodeList: the list of names of the nodes to call the API for. * @param relativeUrlList: the list of relative Urls of nodes after "/api/nodes/<nodeName>/" * @param request: the batch request object. */ delete(nodesList, relativeUrlList, request) { const batchCallRelativeUrl = Net.updateApiVersion20190201(Net.batch); const boundary = MsftSme.newGuid(); const guidsList = this.generateGuidsList(nodesList.length); const guidsToRequestCtxMap = this.generateGuidToRequestContextMap(nodesList, guidsList); // populate request properties. request = this.createBatchRequest(request || {}); // populate batch request headers. this.setRequestHeaders(request, boundary); // Create batch command body. const body = this.createBodySingleMethod(nodesList, relativeUrlList, guidsList, HttpMethod.Delete, boundary, null, request); return this.gateway.post(batchCallRelativeUrl, body, request) .pipe(map((responseData) => { const parsedResponse = this.parseMultiPartResponse(responseData, guidsToRequestCtxMap); return parsedResponse; })); } /** * Adds default parameters to Batch Request. For the outside batch call, we just use the Auth for first node in batch request. * No need to append any tokens for hitting Gateway, as browser handles that. For the nodes being managed, * the tokens are already part of body. * * @param request The batch request object. */ createBatchRequest(request) { if (!request.noAuth) { // Add Node specific authorization handlers request.retryHandlers = (request.retryHandlers || []).concat([{ canHandle: (code, error) => this.authorizationManager.canHandleAjaxFailure(code, error), handle: (code, originalRequest, error) => this.authorizationManager.handleAjaxFailure(code, originalRequest, error) }]); } return request; } /** * Set the request headers for the batch call. * @param request : batch request object. * @param boundary : boundary string used to separate multi part request. */ setRequestHeaders(request, boundary) { // Set Batch request headers. const batchRequest = request; batchRequest.headers = batchRequest.headers || {}; batchRequest.headers[headerConstants.ACCEPT] = 'multipart/mixed'; batchRequest.headers[headerConstants.CONTENT_TYPE] = 'multipart/mixed; boundary={0}'.format(boundary); batchRequest.responseType = 'text'; } /** * Creates http multi-part request body, with each individual request being different Http request type. * * @param nodesList The list of target nodes. * @param relativeUrlList The relative url corresponding to each node * @param requestIdsList The guids list to be used for batch request, corresponding to each node. * @param methodList The Http method list, corresponding to each relative url in relativeUrlList. * @param boundary The boundary string to be used in multipart call * @param commandList The list of command body for each node. * @param request : optional node request options. */ createBody(nodesList, relativeUrlList, requestIdsList, methodList, boundary, commandList, request) { const host = this.gateway.gatewayUrl.replace('http://', '').replace('https://', ''); const body = []; for (let index = 0; index < nodesList.length; index++) { const nodeName = nodesList[index]; const relativeUrl = relativeUrlList[index]; const method = methodList[index]; const requestId = requestIdsList[index]; if (commandList && commandList.length === nodesList.length) { this.createAndAddSinglePart(nodeName, relativeUrl, body, host, method, boundary, requestId, commandList[index], request); } else { this.createAndAddSinglePart(nodeName, relativeUrl, body, host, method, boundary, requestId, null, request); } } body.push('--' + boundary + '--'); return body.join('\r\n'); } /** * Creates http multi-part request body using same Http method for all parts. * * @param nodesList The list of target nodes. * @param relativeUrlList The relative url corresponding to each node * @param requestIdsList The guids list to be used for batch request, corresponding to each node. * @param method The Http method to be used for the call. * @param boundary The boundary string to be used in multipart call. * @param commandList The list of command body for each node. * @param request : optional node request options. */ createBodySingleMethod(nodesList, relativeUrlList, requestIdsList, method, boundary, commandList, request) { const host = this.gateway.gatewayUrl.replace('http://', '').replace('https://', ''); const body = []; for (let index = 0; index < nodesList.length; index++) { const nodeName = nodesList[index]; const relativeUrl = relativeUrlList[index]; const requestId = requestIdsList[index]; if (commandList && commandList.length === nodesList.length) { this.createAndAddSinglePart(nodeName, relativeUrl, body, host, method, boundary, requestId, commandList[index], request); } else { this.createAndAddSinglePart(nodeName, relativeUrl, body, host, method, boundary, requestId, null, request); } } body.push('--' + boundary + '--'); return body.join('\r\n'); } /** * Create the part for a single request and add it to to the multi-Part body. * * @param nodeName : node being targeted with the request. Used for Auth headers. * @param relativeUrl : the relative url of node for Delete request. * @param body : the HTTP request body to populate. * @param host : Host to run request against. * @param method The Http method to be used for the part. * @param boundary The boundary string to be used in multipart call. * @param requestId The request Id to be used for the part call. * @param commandBody : the command body to use for this part/node. * @param request : optional node request options. */ createAndAddSinglePart(nodeName, relativeUrl, body, host, method, boundary, requestId, commandBody, request) { body.push('--' + boundary); body.push(`${headerConstants.CONTENT_TYPE}: application/http; msgtype=request`); body.push('Content-Transfer-Encoding: binary\r\n'); if (method === HttpMethod.Get) { this.addGetCommand(nodeName, relativeUrl, body, host, requestId, request); } else if (method === HttpMethod.Put) { this.addPutCommand(nodeName, relativeUrl, body, host, requestId, commandBody, request); } else if (method === HttpMethod.Post) { this.addPostCommand(nodeName, relativeUrl, body, host, requestId, commandBody, request); } else if (method === HttpMethod.Delete) { this.addDeleteCommand(nodeName, relativeUrl, body, host, requestId, request); } else { throw new Error(MsftSme.getStrings().MsftSmeShell.Core.Error.BatchUnSupportedInvocation.message.format(method)); } } /** * Create a HTTP Delete request and add it to to the multi-Part body. * * @param nodeName : node being targeted with the request. Used for Auth headers. * @param relativeUrl : the relative url of node for Delete request. * @param body : the HTTP request body to populate with Delete command. * @param host : Host to run request against. * @param requestId The request Id to be used for the part call. * @param request : optional node request options. */ addDeleteCommand(nodeName, relativeUrl, body, host, requestId, request) { const multiPartItemUrl = this.getNodeUrl(relativeUrl, nodeName, HttpMethod.Delete); body.push(multiPartItemUrl); this.writeCommonSection(nodeName, body, host, requestId, request); body.push('\r\n'); } /** * Create a HTTP Get request and add it to to the multi-Part body. * * @param nodeName : node being targeted with the request. Used for Auth headers. * @param relativeUrl : the relative url of node for Get request. * @param body : the HTTP request body to populate with Get command. * @param host : Host to run request against. * @param requestId The request Id to be used for the part call. * @param request : optional node request options. */ addGetCommand(nodeName, relativeUrl, body, host, requestId, request) { const multiPartItemUrl = this.getNodeUrl(relativeUrl, nodeName, HttpMethod.Get); body.push(multiPartItemUrl); this.writeCommonSection(nodeName, body, host, requestId, request); body.push('\r\n'); } /** * Create a HTTP Post request and add it to to the multi-Part body. * * @param nodeName : node being targeted with the request. Used for Auth headers. * @param relativeUrl : the relative url of node for Post request. * @param body : the HTTP request body to populate with Post command. * @param host : Host to run request against. * @param requestId The request Id to be used for the part call. * @param data : optional data for the Post request. * @param request : optional node request options. */ addPostCommand(nodeName, relativeUrl, body, host, requestId, data, request) { const multiPartItemUrl = this.getNodeUrl(relativeUrl, nodeName, HttpMethod.Post); body.push(multiPartItemUrl); this.writeCommonSection(nodeName, body, host, requestId, request); body.push(`${headerConstants.CONTENT_TYPE}: application/json; charset=utf-8`); body.push(`${headerConstants.ACCEPT}: application/json, text/plain, */*`); body.push('\r\n'); body.push(data); } /** * Create a HTTP Put request and add it to to the multi-Part body. * * @param nodeName : node being targeted with the request. Used for Auth headers. * @param relativeUrl : the relative url of node for Put request. * @param body : the HTTP request body to populate with PUT command. * @param host : Host to run request against. * @param requestId The request Id to be used for the part call. * @param data : optional data for the Put request. * @param request : optional node request options. */ addPutCommand(nodeName, relativeUrl, body, host, requestId, data, request) { const multiPartItemUrl = this.getNodeUrl(relativeUrl, nodeName, HttpMethod.Put); body.push(multiPartItemUrl); this.writeCommonSection(nodeName, body, host, requestId, request); if (!data) { body.push('\r\n'); } else { body.push(`${headerConstants.CONTENT_TYPE}: application/json; charset=utf-8`); body.push(`${headerConstants.ACCEPT}: application/json, text/plain, */*`); body.push('\r\n'); body.push(data); } } /** * Write common session to body for all HTTP request types. * @param nodeName : node being targeted with the request. * @param body : The body string of the multi-part request being formed. * @param host : The host node(gateway node) for the batch call. * @param requestId The request Id to be used for the part call. * @param request : optional. The node request options. */ writeCommonSection(nodeName, body, host, requestId, request) { body.push('Host: ' + host); body.push('request-id: ' + requestId); body.push(headerConstants.MODULE_NAME + ': ' + EnvironmentModule.getModuleName()); body.push(headerConstants.MODULE_VERSION + ': ' + EnvironmentModule.getModuleVersion()); if (request) { if (request.logAudit === true || request.logAudit === false) { body.push(headerConstants.LOG_AUDIT + (request.logAudit ? ': true' : ': false')); } if (request.logTelemetry === true || request.logTelemetry === false) { body.push(headerConstants.LOG_TELEMETRY + (request.logTelemetry ? ': true' : ': false')); } const endpoint = this.authorizationManager.getJeaEndpoint(nodeName); if (request.powerShellEndpoint) { body.push(headerConstants.POWERSHELL_ENDPOINT + ': ' + request.powerShellEndpoint); } else if (endpoint) { body.push(headerConstants.POWERSHELL_ENDPOINT + ': ' + endpoint); } } this.authorizationManager.addAuthorizationTokensToMultiPartBody(body, nodeName, (request && request.authToken)); } /** * Creates a full Node url for multiPart call * Ex: PUT /api/nodes/<nodeName>/<relativeUrl> HTTP/1.1 * * @param relativeUrl the relative Url after "/nodes" * @param nodeName the name of the node to make a call against * @param actionName the Http call type: Put/Post/Delete/Get. */ getNodeUrl(relativeUrl, nodeName, actionName) { if (!relativeUrl.startsWith('/')) { relativeUrl = `/${relativeUrl}`; } const nodesUrl = Net.updateApiVersion20190201(`nodes/${nodeName}${relativeUrl}`); const fullRelativeUrl = Net.apiRoot.format(nodesUrl); return Net.multiPartCallBodyUrl.format(actionName, fullRelativeUrl); } /** * Creates a list of guids to be used as request-ids in batch request. * * @param count the count of guids to be produced * @return the array of generated guids */ generateGuidsList(count) { const guidsList = []; for (let index = 0; index < count; index++) { guidsList.push(MsftSme.newGuid()); } return guidsList; } /** * Creates a map of guids to NodeNames and sequence number, to be used to parse responses. * * @param orderedNodesList the list of Nodes to run batch against. * @param guidsList the list of guids to be used for requests. * @return the map of generated guids to BatchRequestContext */ generateGuidToRequestContextMap(orderedNodesList, guidsList) { if (orderedNodesList.length !== guidsList.length) { throw new Error(); } const guidToRequestCtxMap = {}; for (let index = 0; index < orderedNodesList.length; index++) { const sequenceNumber = index + 1; const nodeName = orderedNodesList[index]; guidToRequestCtxMap[guidsList[index]] = { sequenceNumber, nodeName }; } return guidToRequestCtxMap; } /** * Parses http response. * See http://stackoverflow.com/questions/21229418/how-to-process-parse-read-a-multipart-mixed-boundary-batch-response * for sample response. * @param responseData: multipart response as received from the Batch call. * @param guidToRequestCtxMap: the map of request-id guids to BatchRequestContext. */ parseMultiPartResponse(responseData, guidToRequestCtxMap) { // ToDo: Check if we can update Gateway connection to get handle of response header, so we can extract the boundary from there. // Try to get boundary string from the response. const indexBoundaryStart = responseData.indexOf('--'); const indexBoundaryEnd = responseData.indexOf('\r\n'); if (indexBoundaryStart < 0 || indexBoundaryEnd < 0 || indexBoundaryStart > indexBoundaryEnd) { Logging.log({ source: 'Batch PowerShell', level: LogLevel.Error, message: MsftSme.getStrings().MsftSmeShell.Core.Error.BatchResponseParsing.message .format(indexBoundaryStart, indexBoundaryEnd) }); return []; } const boundary = responseData.slice(indexBoundaryStart, indexBoundaryEnd); const items = responseData.split(boundary); const results = []; for (const item of items) { if (item === '' || item === '--\r\n') { continue; } const rows = item.split('\r\n'); let status; let data; let requestId; for (const row of rows) { if (row.startsWith('HTTP/')) { const values = row.split(' '); status = +values[1]; } else if (row.toLowerCase().startsWith('request-id')) { const values = row.split(' '); requestId = values[1]; if (!requestId) { Logging.log({ source: 'Batch PowerShell', level: LogLevel.Warning, message: `Couldn't parse request-id from the response: ${item}` }); } } else if (row.startsWith('{') && row.endsWith('}')) { try { // try parse only when we have a valid return code. if (!!status && status < 400) { data = JSON.parse(row); } else { // error response also JSON format mostly. data = row; if (typeof row === 'string') { try { data = JSON.parse(row); } catch { // ignore } } } } catch (exception) { // Log Exception on JSON parse fail. Logging.log({ source: 'Batch PowerShell', level: LogLevel.Error, message: exception.message }); // re throw. throw exception; } } } const response = { status: status, response: data }; const sequenceNumber = guidToRequestCtxMap[requestId]?.sequenceNumber; const nodeName = guidToRequestCtxMap[requestId]?.nodeName; results.push({ response, nodeName, sequenceNumber }); } return results; } } //# sourceMappingURL=batch-connection.js.map