gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
466 lines (410 loc) • 16.6 kB
JavaScript
/* Things related to the operating system.
*
* Copyright 2017 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 ref = require("ref-napi"),
logging = require("./logging.js"),
winapi = require("./winapi.js"),
path = require("path");
var windows = {
winapi: winapi
};
/**
* Determine if this process is running as a service.
*
* @return {Boolean} true if running as a service.
*/
windows.isService = function () {
// Services run in session 0
var sessionId = ref.alloc(winapi.types.DWORD);
var success = winapi.kernel32.ProcessIdToSessionId(process.pid, sessionId);
if (!success) {
throw windows.win32Error("ProcessIdToSessionId", success);
}
return sessionId.deref() === 0;
};
/**
* Returns an Error containing the arguments.
*
* @param {String} message The message.
* @param {String|Number} returnCode [optional] The return code.
* @param {String|Number} errorCode [optional] The last win32 error (from GetLastError), if already known.
* @return {Error} The error.
*/
windows.win32Error = function (message, returnCode, errorCode) {
return winapi.error(message, returnCode, errorCode);
};
/**
* Get the user token for the current process.
*
* This token must be closed with closeToken when no longer needed.
*
* @return {Number} The token handle.
*/
windows.getOwnUserToken = function () {
// It's possible to just call GetCurrentProcessToken, but that returns a pseudo handle that doesn't have the
// required permission to start a process as that user.
// A pseudo handle - doesn't need to be closed;
var processHandle = winapi.kernel32.GetCurrentProcess();
// Enough for CreateProcessAsUser
var access = winapi.constants.TOKEN_ASSIGN_PRIMARY | winapi.constants.TOKEN_DUPLICATE
| winapi.constants.TOKEN_QUERY;
var tokenBuf = ref.alloc(winapi.types.HANDLE);
var success = winapi.advapi32.OpenProcessToken(processHandle, access, tokenBuf);
if (!success) {
throw winapi.error("OpenProcessToken failed");
}
return tokenBuf.deref();
};
/**
* Closes a user token.
* @param {Number} userToken The user token.
*/
windows.closeToken = function (userToken) {
if (userToken) {
winapi.kernel32.CloseHandle(userToken);
}
};
/**
* Gets the user token for the active desktop session.
*
* This token must be closed with closeToken when no longer needed.
*
* @return {Number} The token, 0 if there is no active desktop session.
*/
windows.getDesktopUser = function () {
var userToken;
if (windows.isService()) {
// Get the session ID of the console session.
var sessionId = winapi.kernel32.WTSGetActiveConsoleSessionId();
logging.debug("session id:", sessionId);
if (sessionId === 0xffffffff) {
// There isn't a session.
userToken = 0;
} else {
// Get the access token of the user logged into the session.
var tokenBuf = ref.alloc(winapi.types.HANDLE);
var success = winapi.wtsapi32.WTSQueryUserToken(sessionId, tokenBuf);
if (success) {
userToken = tokenBuf.deref();
} else {
var errorCode = winapi.kernel32.GetLastError();
logging.warn("WTSQueryUserToken failed (win32=" + errorCode + ")");
switch (errorCode) {
case winapi.errorCodes.ERROR_NO_TOKEN:
case winapi.errorCodes.ERROR_SUCCESS:
// There is no user on this session.
userToken = 0;
break;
case winapi.errorCodes.ERROR_ACCESS_DENIED:
case winapi.errorCodes.ERROR_PRIVILEGE_NOT_HELD:
// Not running as a service?
throw winapi.error("WTSQueryUserToken (isService may be wrong)", errorCode);
break;
default:
throw winapi.error("WTSQueryUserToken", errorCode);
break;
}
}
}
} else {
// If not running as a service, then assume the current user is the desktop user.
userToken = windows.getOwnUserToken();
}
return userToken;
};
/**
* Determines if the active console session is a user logged on.
* @return {Boolean} true if the active console session is a user logged on.
*/
windows.isUserLoggedOn = function () {
var token = windows.getDesktopUser();
var loggedOn = !!token;
if (token) {
windows.closeToken(token);
}
return loggedOn;
};
/**
* Gets the environment variables for the specified user.
*
* @param {Number} token Token handle for the user.
* @return {Array<String>} An array of strings for each variable, in the format of "name=value"
*/
windows.getEnv = function (token) {
var envPtr = ref.alloc(winapi.types.LP);
var success = winapi.userenv.CreateEnvironmentBlock(envPtr, token, false);
if (!success) {
throw winapi.error("CreateEnvironmentBlock");
}
return winapi.stringFromWideCharArray(envPtr.deref(), true);
};
/**
* Gets the GPII data directory for the specified user (identified by token).
*
* When running as a service, this process's "APPDATA" value will not point to the current user's.
*
* @param {Number} userToken Token handle for the user.
* @return {String} The GPII data directory for the given user.
*/
windows.getUserDataDir = function (userToken) {
// Search the environment block for the APPDATA value. (A better way would be to use SHGetKnownFolderPath)
var env = windows.getEnv(userToken);
var appData = null;
for (var n = 0, len = env.length; n < len; n++) {
var match = env[n].match(/^APPDATA=(.*)/i);
if (match) {
appData = match[1];
break;
}
}
return appData && path.join(appData, "GPII");
};
/**
* Returns a promise that resolves when a process has terminated, or after the given timeout.
*
* @param {Number} pid The process ID.
* @param {Number} timeout Milliseconds to wait before timing out. (default: infinate)
* @return {Promise} Resolves when the process has terminated, or when timed out (with a value of "timeout"). Rejects
* upon failure.
*/
windows.waitForProcessTermination = function (pid, timeout) {
return new Promise(function (resolve, reject) {
var hProcess = winapi.kernel32.OpenProcess(winapi.constants.SYNCHRONIZE, 0, pid);
if (!hProcess) {
reject(windows.win32Error("OpenProcess"));
} else {
if (!timeout && timeout !== 0) {
timeout = winapi.constants.INFINITE;
}
winapi.kernel32.WaitForSingleObject.async(hProcess, timeout, function (err, ret) {
winapi.kernel32.CloseHandle(hProcess);
switch (ret) {
case winapi.constants.WAIT_OBJECT_0:
resolve();
break;
case winapi.constants.WAIT_TIMEOUT:
resolve("timeout");
break;
case winapi.constants.WAIT_FAILED:
default:
reject(windows.win32Error("WaitForSingleObject", ret));
break;
}
});
}
});
};
/**
* Waits until one of the Win32 objects in an array are in the signalled state, resolving with that handle (or
* "timeout").
*
* Wrapper for WaitForMultipleObjects (https://msdn.microsoft.com/library/ms687025)
*
* @param {Array<Number>} handles The win32 handles to wait on.
* @param {Number} timeout [Optional] The timeout, in milliseconds. (default: infinite)
* @param {Boolean} waitAll [Optional] Wait for all handles to be signalled, instead of just one.
* @return {Promise} Resolves with the handle that triggered, "timeout", or "all" if waitAll is true.
*/
windows.waitForMultipleObjects = function (handles, timeout, waitAll) {
return new Promise(function (resolve, reject) {
if (!Array.isArray(handles)) {
reject({
message: "handles must be an array",
isError: true
});
return;
}
// Use a copy the handle array, so it can't be modified after the function returns.
handles = handles.slice();
if (!timeout && timeout !== 0) {
timeout = winapi.constants.INFINITE;
}
winapi.kernel32.WaitForMultipleObjects.async(handles.length, handles, waitAll, timeout,
function (err, ret) {
if (err) {
reject(err);
} else {
switch (ret) {
case winapi.constants.WAIT_TIMEOUT:
resolve("timeout");
break;
case winapi.constants.WAIT_FAILED:
// GetLastError will not work, because WaitForMultipleObjects is called in a different thread.
// Call WaitForMultipleObjects again, but in this thread (with a short timeout in case it works)
var newRet = winapi.kernel32.WaitForMultipleObjects(
handles.length, handles, waitAll, 1);
var errorCode = winapi.kernel32.GetLastError();
var message = "WAIT_FAILED";
if (newRet !== ret) {
message += " 2nd return:" + newRet;
}
reject(winapi.error("WaitForMultipleObjects:" + message, ret, errorCode));
break;
default:
// The return is the handle index that triggered the return, offset by WAIT_OBJECT_0 or WAIT_ABANDONED_0
var index = (ret < winapi.constants.WAIT_ABANDONED_0)
? ret - winapi.constants.WAIT_OBJECT_0
: ret - winapi.constants.WAIT_ABANDONED_0;
if (index < 0 || index >= handles.length) {
// Unknown return
reject(winapi.win32Error("WaitForMultipleObjects", ret));
} else {
resolve(handles[index]);
}
break;
}
}
});
});
};
/**
* Gets the security identifier (SID) from a user token.
*
* @param {Integer} token The user token.
* @return {*} The SID of the user.
*/
windows.getSidFromToken = function (token) {
// winnt.h:
var TokenUser = 1;
var lengthBuffer = ref.alloc(winapi.types.DWORD);
// // Get the length
var success = winapi.advapi32.GetTokenInformation(token, TokenUser, ref.NULL, 0, lengthBuffer);
if (!success) {
var err = winapi.kernel32.GetLastError();
// ERROR_INSUFFICIENT_BUFFER is expected.
if (err !== winapi.errorCodes.ERROR_INSUFFICIENT_BUFFER) {
throw winapi.error("GetTokenInformation", success);
}
}
// GetTokenInformation fills a TOKEN_USER structure, which contains another struct containing a pointer to the SID
// and a dword. The sid pointer points to a chunk of data, which is located after the struct.
var length = lengthBuffer.deref();
var tokenUserBuffer = Buffer.alloc(length);
// Get the sid data.
success = winapi.advapi32.GetTokenInformation(token, TokenUser, tokenUserBuffer, length, lengthBuffer);
if (!success) {
throw winapi.error("GetTokenInformation", success);
}
// Take the SID from the buffer.
var TokenUserHeader = 2 * ref.types["int"].size;
var sid = tokenUserBuffer.slice(TokenUserHeader);
return sid;
};
/**
* Set the permissions of the pipe so the logged in user can access it.
*
* This connects to the pipe, modifies the ACL to include the desktop user's security descriptor, then closes the pipe.
*
* @param {String} pipeName Name of the pipe.
*/
windows.setPipePermissions = function (pipeName) {
// winnt.h
var FILE_GENERIC_READ = 0x120089;
var FILE_GENERIC_WRITE = 0x120116;
var DACL_SECURITY_INFORMATION = 0x4;
// AccCtl.h
var SE_KERNEL_OBJECT = 0x6;
var GRANT_ACCESS = 1;
var TRUSTEE_IS_SID = 0;
var TRUSTEE_IS_USER = 1;
var token = windows.getDesktopUser();
var sid;
try {
sid = windows.getSidFromToken(token);
} finally {
windows.closeToken(token);
}
var pipeHandle = null;
try {
// Open the pipe.
var pipeNameBuf = winapi.stringToWideChar(pipeName);
pipeHandle = winapi.kernel32.CreateFileW(
pipeNameBuf, (winapi.constants.GENERIC_READ | winapi.constants.WRITE_DAC) >>> 0, 0,
ref.NULL, winapi.constants.OPEN_EXISTING, ref.NULL, ref.NULL);
if (pipeHandle === winapi.constants.INVALID_HANDLE_VALUE) {
throw winapi.error("CreateFile", pipeHandle);
}
// Get the ACL.
var daclP = ref.alloc(winapi.PACL);
daclP.ref().fill(0);
var result = winapi.advapi32.GetSecurityInfo(pipeHandle, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,
ref.NULL, ref.NULL, daclP, ref.NULL, ref.NULL);
var dacl = daclP.deref();
if (result) {
throw winapi.error("GetSecurityInfo", result);
}
// Add the user to the ACL.
var access = new winapi.EXPLICIT_ACCESS();
access.ref().fill(0);
access.grfAccessMode = GRANT_ACCESS;
access.grfAccessPermissions = (FILE_GENERIC_READ | FILE_GENERIC_WRITE);
access.grfInheritance = 0;
access.Trustee.pMultipleTrustee = ref.NULL;
access.Trustee.MultipleTrusteeOperation = 0;
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
access.Trustee.TrusteeType = TRUSTEE_IS_USER;
access.Trustee.ptstrName = sid;
var newDacl = ref.alloc(winapi.PACL);
newDacl.ref().fill(0);
result = winapi.advapi32.SetEntriesInAclW(1, access.ref(), dacl, newDacl);
if (result) {
throw winapi.error("SetEntriesInAclW", result);
}
// Set the ACL.
result = winapi.advapi32.SetSecurityInfo(pipeHandle, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,
ref.NULL, ref.NULL, newDacl.deref(), ref.NULL);
if (result) {
throw winapi.error("SetSecurityInfo", result);
}
} finally {
if (pipeHandle) {
winapi.kernel32.CloseHandle(pipeHandle);
}
}
};
/**
* Expands the environment variables in a string, which are surrounded by '%'.
* For example, the input string of "%SystemRoot%\System32" returns "C:\Windows\System32".
*
* @param {String} input The input string.
* @return {String} The input string with the environment variables expanded.
*/
windows.expandEnvironmentStrings = function (input) {
var result;
if (input && input.length > 0) {
var inputBuffer = winapi.stringToWideChar(input);
// Initial buffer of MAX_PATH should be big enough for most cases (assuming this function is called for paths).
var len = Math.max(winapi.constants.MAX_PATH + 1, input.length + 20);
var outputBuffer = Buffer.alloc((len + 1) * 2);
// Expand the variables
var requiredSize = winapi.kernel32.ExpandEnvironmentStringsW(inputBuffer, outputBuffer, len);
if (requiredSize > len) {
// Initial buffer is too small - call again with the correct size.
len = requiredSize;
outputBuffer = Buffer.alloc((len + 1) * 2);
requiredSize = winapi.kernel32.ExpandEnvironmentStringsW(inputBuffer, outputBuffer, len);
}
if (requiredSize === 0) {
throw winapi.error("ExpandEnvironmentStringsW", requiredSize);
}
result = winapi.stringFromWideChar(outputBuffer);
} else {
result = "";
}
return result;
};
module.exports = windows;