UNPKG

gpii-windows

Version:

Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™

361 lines (316 loc) 11.6 kB
/* * 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; };