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