gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
361 lines (316 loc) • 11.6 kB
JavaScript
/*
* Windows Service interface.
* This connects to and authenticates with the Windows service. The GPII_SERVICE_PIPE environment variable is used to
* specify the name of the pipe to connect to.
*
* Copyright 2018 Raising the Floor - International
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The R&D leading to these results received funding from the
* Department of Education - Grant H421A150005 (GPII-APCP). However,
* these results do not necessarily represent the policy of the
* Department of Education, and you should not assume endorsement by the
* Federal Government.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
"use strict";
var fluid = require("gpii-universal"),
net = require("net");
var gpii = fluid.registerNamespace("gpii");
fluid.registerNamespace("gpii.windows.service.serviceHandler");
require("../../WindowsUtilities/WindowsUtilities.js");
var messaging = fluid.require("%gpii-windows/gpii-service/shared/pipe-messaging.js");
fluid.defaults("gpii.windows.service", {
gradeNames: ["fluid.component"],
components: {
serviceHandler: {
type: "gpii.windows.service.serviceHandler"
}
},
events: {
onServiceReady: "{serviceHandler}.events.onConnected"
},
distributeOptions: {
// Make the accessRequester use the service to get the client credentials.
accessRequester: {
record: "gpii.windows.service.clientCredentialsSource",
target: "{that gpii.flowManager.settingsDataSource}.options.components.accessRequester.options.clientCredentialDataSourceGrade"
}
}
});
fluid.makeGradeLinkage("gpii.windows.serviceLinkage",
["gpii.flowManager.local"],
"gpii.windows.service"
);
// Manages the connection to the Windows service.
fluid.defaults("gpii.windows.service.serviceHandler", {
gradeNames: ["fluid.component", "fluid.resolveRootSingle"],
singleRootType: "gpii.windows.service.serviceHandler",
components: {
requestHandler: {
type: "gpii.windows.service.requestHandler"
},
requestSender: {
type: "gpii.windows.service.requestSender"
}
},
events: {
"onConnected": null,
"onPipeClose": null
},
listeners: {
"onCreate.connectToService": "{that}.connectToService",
"onPipeClose": {
"funcName": "gpii.windows.service.servicePipeClosed",
"args": ["{that}", "{arguments}.0"]
},
"onDestroy.closePipe": {
"funcName": "gpii.windows.service.closePipe",
"args": ["{that}"]
}
},
invokers: {
connectToService: {
funcName: "gpii.windows.service.connectToService",
args: ["{that}"]
}
},
members: {
/** true when connected */
connected: false,
/** When the successful connection was made. */
connectTime: null,
/** Number of connection failures. */
failureCount: 0,
/**
* The connection to the service.
* @type net.Socket
*/
pipe: null,
/**
* The messaging session.
* @type Session
*/
session: null
},
pipePrefix: "\\\\.\\pipe\\gpii-",
/** true to reconnect if the pipe had disconnected */
reconnect: true
});
/**
* Connect to the service, as specified by the GPII_SERVICE_PIPE environment variable, which is expected to be in the
* form of "pipe:<pipe-id>".
*
* The full pipe name will then consist of: \\.\pipe\gpii-<pipe-id>.
*
* @param {Component} that The gpii.serviceHandler instance.
* @return {Promise} Resolves when the connection is complete (and authenticated).
*/
gpii.windows.service.connectToService = function (that) {
var pipeId;
if (process.env.GPII_SERVICE_PIPE) {
var match = process.env.GPII_SERVICE_PIPE.match(/^pipe:([^\\/\s]{1,200})\s*$/);
if (match && match[1]) {
pipeId = match[1];
} else {
fluid.log(fluid.logLevel.WARN, "GPII_SERVICE_PIPE is badly formed: " + process.env.GPII_SERVICE_PIPE);
}
} else {
pipeId = "gpii";
}
var promise;
if (pipeId) {
var pipeName = that.options.pipePrefix + pipeId;
fluid.log(fluid.logLevel.IMPORTANT, "Service: Connecting to " + pipeName);
// Connect to the pipe.
promise = gpii.windows.service.serviceAuthenticate(that, pipeName).then(function (data) {
that.connectTime = process.hrtime();
that.connected = true;
that.pipe.on("error", function (err) {
fluid.log("Service: Pipe error: ", err);
that.pipe.destroy();
});
that.pipe.on("close", function () {
fluid.log("Service: Pipe closed");
that.events.onPipeClose.fire();
that.pipe.destroy();
});
that.pipe.on("end", function () {
that.pipe.destroy();
});
fluid.log("Service: Authenticated");
that.session = messaging.createSession(that.pipe, "gpii", that.requestHandler.handleRequest, data);
that.session.on("ready", function () {
fluid.log("Service: Ready");
that.events.onConnected.fire();
});
}, that.events.onPipeClose.fire);
} else {
fluid.log("Service: Not connecting to the service.");
promise = fluid.promise();
promise.reject();
}
return promise;
};
/**
* Authenticate with the service.
*
* @param {Component} that The gpii.serviceHandler instance.
* @param {String} pipeName The pipe name.
* @return {Promise} Resolves when the connection is complete (and authenticated), with any data that was after the
* challenge.
*/
gpii.windows.service.serviceAuthenticate = function (that, pipeName) {
var promise = fluid.promise();
var allData = null;
var authDone = false;
var pipe = net.connect(pipeName, function () {
fluid.log("Service: Connected");
});
pipe.on("data", function (data) {
allData = allData ? Buffer.concat([allData, data]) : data;
// There's a chance that extra data could be after the challenge, so only take what's needed.
var eol;
while ((eol = allData.indexOf("\n".charCodeAt(0))) >= 0) {
var line = allData.toString("utf8", 0, eol);
allData = allData.slice(eol + 1);
var authenticated = false;
if (line === "OK") {
// Service responds with "OK" when authenticated.
authenticated = true;
} else if (line.startsWith("challenge:") && !authDone) {
var match = line.match(/^challenge:([0-9]+|none)$/);
if (!match || !match[1]) {
promise.reject("Invalid authentication challenge: '" + line + "'");
break;
}
var challenge = match[1];
if (challenge !== "none") {
// Call set event, to prove this process is the pipe client.
gpii.windows.service.serviceChallenge(challenge);
}
authDone = true;
} else if (line.length > 0) {
promise.reject("Unexpected data from service: " + line);
break;
}
if (authenticated) {
promise.resolve(allData);
break;
}
}
});
var pipeProblem = function (err) {
fluid.log("Service: Unable to authenticate: ", err || "Pipe closed");
if (promise.disposition) {
pipe.destroy();
} else {
promise.reject({
isError: !!err,
message: err ? "Pipe error" : "Pipe closed",
error: err
});
}
};
pipe.on("close", pipeProblem);
pipe.on("error", pipeProblem);
pipe.on("end", pipeProblem);
return promise.then(function () {
that.pipe = pipe;
pipe.removeAllListeners();
}, function (err) {
fluid.log("Service: Authentication failed:", err);
pipe.removeAllListeners();
pipe.destroy();
});
};
/**
* Performs the authentication challenge presented by the service upon connecting.
*
* The challenge data is an event handle, with which SetEvent is called. Only this process is able to use this handle,
* and the service will know when it's been called (GPII-2399).
*
* @param {String} challenge The challenge data.
*/
gpii.windows.service.serviceChallenge = function (challenge) {
var eventHandle = parseInt(challenge);
gpii.windows.kernel32.SetEvent(eventHandle);
};
/**
* Closes the pipe.
*
* @param {Component} that The gpii.serviceHandler instance.
*/
gpii.windows.service.closePipe = function (that) {
if (that.pipe) {
that.pipe.end();
}
};
/**
* Called when the pipe has been closed.
*
* @param {Component} that The gpii.serviceHandler instance.
*/
gpii.windows.service.servicePipeClosed = function (that) {
if (that.connected) {
that.connected = false;
}
if (that.reconnect) {
// Throttle the retry attempts by a few seconds for each failure, unless the last connection was above 30 seconds.
var duration = that.connectTime && process.hrtime(that.connectTime);
if (duration && duration[0] > 30) {
that.failureCount = 0;
} else {
that.failureCount++;
}
var delay = Math.min(30, that.failureCount * 5);
fluid.log("Service: Retrying connection" + (delay ? " in " + delay + " seconds" : ""));
setTimeout(that.connectToService, delay * 1000);
}
};
// Data source for the client credentials, which takes it from the secrets file via the service.
fluid.defaults("gpii.windows.service.clientCredentialsSource", {
gradeNames: ["fluid.component"],
components: {
fallbackDataSource: {
type: "gpii.accessRequester.clientCredentialDataSource.file"
}
},
invokers: {
get: {
funcName: "gpii.windows.service.getClientCredentials",
args: ["{gpii.windows.service.serviceHandler}", "{that}.fallbackDataSource"]
}
}
});
/**
* Gets the client credentials from the service.
*
* @param {Component} service The service handler instance.
* @param {Component} fallbackDataSource The datasource for client credentials, if the service doesn't have any (perhaps
* due to the secrets file not being installed).
* @return {Promise} Resolves with the client credentials.
*/
gpii.windows.service.getClientCredentials = function (service, fallbackDataSource) {
var promiseTogo = fluid.promise();
var credentialsPromise = fluid.promise();
service.requestSender.getClientCredentials().then(function (clientCredentials) {
if (clientCredentials) {
credentialsPromise.resolve(clientCredentials);
} else {
credentialsPromise.reject({
isError: true,
message: "Service returned no client credentials"
});
}
}, credentialsPromise.reject);
credentialsPromise.then(promiseTogo.resolve, function () {
// Use the fallback credentials source.
fluid.promise.follow(fallbackDataSource.get(), promiseTogo);
});
return promiseTogo;
};