diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..5f32c90 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,30 @@ +name: CodeQL + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + branches: [main] + schedule: + - cron: '8 8 8 * *' + +jobs: + Analyze: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: javascript + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/git-diff-check.yml b/.github/workflows/git-diff-check.yml new file mode 100644 index 0000000..0fb1760 --- /dev/null +++ b/.github/workflows/git-diff-check.yml @@ -0,0 +1,24 @@ +name: Git diff Check + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + branches: [main] + +jobs: + Analyze: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Run git diff check + run: | + git config --global core.whitespace cr-at-eol,tab-in-indent + git diff --check HEAD^ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0681f4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +package-lock.json +yarn.lock +/src/daemon/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8050a63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2022 blu3mania. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 665d0d5..2236341 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,172 @@ -# registry-auto-updater -Auto update registry values based on trigger or timer +# Registry-Auto-Updater +[![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-yellow)](https://raw.githubusercontent.com/blu3mania/registry-auto-updater/main/LICENSE) +[![node.js 17+](https://img.shields.io/badge/node.js-17.0.0-blue?logo=node.js)](https://nodejs.org/en/) +[![Latest Release](https://img.shields.io/github/v/release/blu3mania/registry-auto-updater)](https://github.com/blu3mania/registry-auto-updater/releases/latest) + +Automatically update registry values based on trigger or timer. + +It can be run as a standalone application or as a Windows service. + +## Run these steps first: + +1. One of the packages, "ffi-napi", uses native modules and relies on "node-gyp" to build the project. As a + result, there are some prerequisites that need to be installed/configured. Please refer to [node-gyp's + instructions](https://github.com/nodejs/node-gyp#installation). +2. Edit src/settings.json. + * service defines service parameters when installed as Windows service: + * name is the service name to be used. + * account info is optional. If provided, the service will be running as the specified account. These properties + can be provided: + * name is account's name + * password is account's password + * domain is optional, and should be provided if the account is a domain account + ``` + "service": { + "name": "Registry Auto Updater", + "account": { + "name": "{account name}", + "password": "{account password}", + "domain": "{account domain}" + } + }, + ``` + * showNotification allows showing Windows notification when an action is taken, such as domain is updated + in provider, or domain update is queued (due to update interval). + + **Note**, this only works when running in standalone mode instead of a Windows service. + * updateSets defines the registry keys/values to monitor/update. It is an array, and each item has the following + properties: + * id is a string identifier used for display, so it is clear which update set is triggered when event happens + * trigger defines the type of trigger used to update the value. Supported values are: "Value Change", "Timer". + If possible, "Value Change" should be used as it automatically detects change of value in teh registry and + resets it back to the defined value. + * timer defines the update interval when trigger type is "Timer". The value is in seconds. If not provided, + default value 60 seconds is used. + * values defines registry keys/values to monitor/update for the given update set. + * key is the path to the registry key. It needs to start with one of the predefined root keys. For root key, + both long and short terms are accepted. E.g. HKEY_LOCAL_MACHINE and HKLM. + * name is the name of the value to be monitored/updated. + * value is the value to be updated to. Its type is determined by the type defined in teh registry: + * REG_DWORD & REG_QWORD: integer number + * REG_SZ & REG_EXPAND_SZ: string + * REG_MULTI_SZ: array of strings + * REG_BINARY: not supported + + Unicode characters are supported for string types. See example below. + * type is optional, as the code can detect it from current key value. However, it is strongly recommended, in + case the key path/name doesn't exist in the registry. + ``` + "updateSets": [ + { + "id": "Interactive logon: Do not require CTRL+ALT+DEL", + "trigger": "Value Change", + "values": [ + { + "key": "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + "name": "DisableCAD", + "value": 1, + "type": "REG_DWORD" + }, + { + "key": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", + "name": "DisableCAD", + "value": 1 + } + ] + }, + { + "id": "Types Example", + "trigger": "Timer", + "timer": 30, + "values": [ + { + "key": "HKLM\\SOFTWARE\\Some Company\\Some Product\\Some Key", + "name": "DWORD Value", + "value": 4294967295, + "type": "REG_DWORD" + }, + { + "key": "HKLM\\SOFTWARE\\Some Company\\Some Product\\Some Key", + "name": "QWORD Value", + "value": 42949672950000, + "type": "REG_QWORD" + }, + { + "key": "HKLM\\SOFTWARE\\Some Company\\Some Product\\Some Key", + "name": "A String", + "value": "Some string", + "type": "REG_SZ" + }, + { + "key": "HKLM\\SOFTWARE\\Some Company\\Some Product\\Some Key", + "name": "An Expandable Path", + "value": "%SystemRoot%\\System32\\SomeBinary.exe", + "type": "REG_EXPAND_SZ" + }, + { + "key": "HKLM\\SOFTWARE\\Some Company\\Some Product\\Some Key", + "name": "List of Strings", + "value": [ + "First string value", + "Second string value", + "Unicode string 🧑‍💻😮‍💨😵‍💫🥴", + "Last string value" + ], + "type": "REG_MULTI_SZ" + }, + ] + } + ``` +3. Run "npm install". Accept UAC prompts if any (there could be up to 4). + + **Notes** + - this step installs the script as a Windows service. If it's not desired, run "npm run uninstall" + afterwards. + - It seems node.js 16.x doesn't always work due to V8 change that enforced one-to-one mapping of Buffers + and backing stores (see https://monorail-prod.appspot.com/p/v8/issues/detail?id=9908). It might crash + like this: + ``` + # + # Fatal error in , line 0 + # Check failed: result.second. + # + # + # + #FailureMessage Object: 000000A5C1FFE530 + 1: 00007FF6B7E1B1EF v8::internal::CodeObjectRegistry::~CodeObjectRegistry+123599 + 2: 00007FF6B7D37E7F std::basic_ostream >::operator<<+65407 + 3: 00007FF6B8A14482 V8_Fatal+162 + 4: 00007FF6B847EC6D v8::internal::BackingStore::Reallocate+637 + 5: 00007FF6B86C81D9 v8::ArrayBuffer::GetBackingStore+137 + 6: 00007FF6B7DEAD29 napi_get_typedarray_info+393 + 7: 00007FF9D7298828 + 8: 00007FF9D7299F88 + 9: 00007FF9D72997CF + 10: 00007FF9D729F786 + 11: 00007FF9D7298063 + 12: 00007FF9D729EFB3 + 13: 00007FF6B7DE54EB node::Stop+32747 + 14: 00007FF6B86FE5EF v8::internal::SetupIsolateDelegate::SetupHeap+53823 + 15: 000001BD57A7603B + ``` + + There have been several issues reported against node.js (e.g. https://github.com/nodejs/node/issues/32463) + and ffi-napi (e.g. https://github.com/node-ffi-napi/node-ffi-napi/issues/188). Even though one of the + reported issues claimed that it was fixed in node.js 16.17.0, the aforementioned crash could still be + encountered. It seems node.js 18.9.0 is quite stable. For now this package is marked as requiring node.js + 17+, but the recommendation is to use the newest 18.x. + + +## To run the script manually: + +Run "npm start" or "node src/app.js". + +## To install and run the script as a Windows service: + +Run "npm run install" or "node src/install-service.js". Accept UAC prompts if any (there could be up to 4). + +**Note**, if settings.json is updated when service is running, restart it in Windows Services control panel. + +## To uninstall the Windows service: + +Run "npm run uninstall" or "node src/uninstall-service.js". Accept UAC prompts if any (there could be up to 4). \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..011ab7c --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "registry-auto-updater", + "type": "module", + "version": "1.0.0", + "description": "Auto update Windows registry based on configuration", + "exports": "src/app.js", + "scripts": { + "start": "node src/app.js", + "postinstall": "node src/install-service.js", + "uninstall": "node src/uninstall-service.js" + }, + "keywords": [ + "auto update", + "registry", + "windows registry", + "registry monitor" + ], + "author": "blu3mania ", + "license": "Apache-2.0", + "homepage": "https://github.com/blu3mania/registry-auto-updater#readme", + "bugs": { + "url": "https://github.com/blu3mania/registry-auto-updater/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/blu3mania/registry-auto-updater.git" + }, + "engines": { + "node": ">=17.0.0" + }, + "os": [ + "win32" + ], + "dependencies": { + "chalk": "^5.0.1", + "ffi-napi": "^4.0.3", + "node-notifier": "^10.0.1", + "node-windows": "^1.0.0-beta.8" + }, + "devDependencies": {} +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..40c3497 --- /dev/null +++ b/src/app.js @@ -0,0 +1,100 @@ +import * as url from 'url'; +import path from 'path'; +import notifier from 'node-notifier'; + +import { + error, + warning, + info, + verbose } from './print.js'; +import { Registry } from './windows-registry.js'; +import settings from './settings.json' assert {type: 'json'}; + +verbose('Starting...'); + +const TriggerType = { + ValueChange: 'value change', + Timer: 'timer', +}; + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +const registry = Registry.instance; +const monitorTokens = []; + +main(); + +function main() { + settings.updateSets.forEach((updateSet) => { + updateSet.values.forEach((value) => { + const updateValue = () => { + return registry.setValue(value.key, value.name, value.value, Registry.ValueType[value.type]); + }; + + // First check if we have write permission on the key by setting its original value + if (!updateValue()) { + error(`Cannot obtain write permission on "${value.name}" of key "${value.key}", exiting...`); + exitProcess(); + } + + if (updateSet.trigger?.toLowerCase() === TriggerType.ValueChange) { + const monitorToken = registry.monitorValue(value.key, value.name, value.value, true, (key, currentValue, compareValue) => { + info(`UpdateSet "${updateSet.id}" triggered. Resetting value "${value.key}\\${value.name}"...`); + if (updateValue()) { + info(`Successfully reset value "${value.key}\\${value.name}".`); + if (settings.showNotification) { + sendDesktopNotification('Monitored Registry Value Changed', `UpdateSet "${updateSet.id}" triggered. Successfully reset value "${value.key}\\${value.name}".`, 'value-updated.png'); + } + } else { + error(`Failed to update value "${value.key}\\${value.name}"!`); + if (settings.showNotification) { + sendDesktopNotification('Monitored Registry Value Changed', `UpdateSet "${updateSet.id}" triggered. Failed to update value "${value.key}\\${value.name}".`, 'value-update-failure.png'); + } + } + }); + monitorTokens.push(monitorToken); + } else if (updateSet.trigger?.toLowerCase() === TriggerType.Timer) { + setInterval(() => { + //info(`UpdateSet "${updateSet.id}" triggered. Updating value...`); + if (updateValue()) { + info(`Successfully reset value "${value.key}\\${value.name}".`); + if (settings.showNotification) { + sendDesktopNotification('Registry Value Monitor Timer Event', `UpdateSet "${updateSet.id}" triggered. Successfully reset value ${value.key}\\${value.name}.`, 'value-updated.png'); + } + } else { + error(`Failed to update value "${value.key}\\${value.name}"!`); + if (settings.showNotification) { + sendDesktopNotification('Registry Value Monitor Timer Event', `UpdateSet "${updateSet.id}" triggered. Failed to update value "${value.key}\\${value.name}".`, 'value-update-failure.png'); + } + } + }, (updateSet.timer ?? 60) * 1000); + } + }); + }); + + process.on('SIGINT', () => { + warning('SIGINT received, exiting...'); + exitProcess(); + }); +} + +function exitProcess() { + monitorTokens.forEach((token) => { + token.stop(); + verbose(`Stopped monitoring ${token.tokenData.path}`); + }); + process.exit(); +} + +function sendDesktopNotification(title, message, icon) { + notifier.notify({ + title: title, + message: message, + appID: 'Update Dynamic DNS', + icon: getImagePath(icon), + }); +} + +function getImagePath(imageFile) { + return path.join(__dirname, 'images', imageFile); +} diff --git a/src/images/value-update-failure.png b/src/images/value-update-failure.png new file mode 100644 index 0000000..c0fbf0a Binary files /dev/null and b/src/images/value-update-failure.png differ diff --git a/src/images/value-updated.png b/src/images/value-updated.png new file mode 100644 index 0000000..d437540 Binary files /dev/null and b/src/images/value-updated.png differ diff --git a/src/install-service.js b/src/install-service.js new file mode 100644 index 0000000..9d0efa2 --- /dev/null +++ b/src/install-service.js @@ -0,0 +1,53 @@ +import * as url from 'url'; +import path from 'path'; +import { Service } from 'node-windows'; + +import { + warning, + info, + verbose } from './print.js'; +import settings from './settings.json' assert {type: 'json'}; + +main(); + +function main() { + // Create a new service object. + const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + const svc = new Service({ + name: settings.service?.name ?? 'Registry Auto Updater', + description: 'Auto update registry values.', + script: `${path.join(__dirname, 'app.js')}`, + nodeOptions: [ + '--harmony', + '--max_old_space_size=4096' + ] + }); + + if (settings.service?.account?.name && settings.service?.account?.password) { + svc.logOnAs.account = settings.service.account.name; + svc.logOnAs.password = settings.service.account.password; + if (settings.service?.account?.domain) { + svc.logOnAs.domain = settings.service.account.domain; + } + } + + // Listen for the "install" event, which indicates the process is available as a service. + svc.on('install', () => { + verbose('Service installed.'); + info('Starting service, please accept UAC prompts if any...'); + svc.start(); + }); + + svc.on('start', () => { + verbose('Service started.'); + }); + + svc.on('alreadyinstalled', () => { + warning('Service is already installed!'); + info('Starting the service in case it is not running, please accept UAC prompts if any...'); + svc.start(); + }); + + info('Installing service, please accept UAC prompts if any...'); + svc.install(); +} diff --git a/src/print.js b/src/print.js new file mode 100644 index 0000000..654ef37 --- /dev/null +++ b/src/print.js @@ -0,0 +1,66 @@ +import chalk from 'chalk'; + +const dateTimeFormatOprions = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false +}; + +/** Internal method: format a message so it shows with timestamp. */ +function formatMessage(msg) { + return `[${new Intl.DateTimeFormat('en-US', dateTimeFormatOprions).format(new Date())}] ${typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)}`; +} + +/** + * Prints a message in console. + * @param {string|Object} msg - The message to be printed. It can be a non-string type, in which case it will be serialized before printing. + * @param {Chalk} color - (Optional) The color of the message to be printed. + * If not provided, default color white is used. + */ +function print(msg, color = chalk.white) { + console.log(color(formatMessage(msg))); +} + +/** + * Prints an error message in console. + * @param {string|Object} msg - The error message to be printed. It can be a non-string type, in which case it will be serialized before printing. + */ +function error(msg) { + console.log(chalk.red(formatMessage(msg))); +} + +/** + * Prints a warning message in console. + * @param {string|Object} msg - The warning message to be printed. It can be a non-string type, in which case it will be serialized before printing. + */ +function warning(msg) { + console.log(chalk.yellow(formatMessage(msg))); +} + +/** + * Prints an infomation message in console. + * @param {string|Object} msg - The information message to be printed. It can be a non-string type, in which case it will be serialized before printing. + */ +function info(msg) { + console.log(chalk.cyan(formatMessage(msg))); +} + +/** + * Prints a verbose message in console. + * @param {string|Object} msg - The verbose message to be printed. It can be a non-string type, in which case it will be serialized before printing. + */ +function verbose(msg) { + console.log(chalk.green(formatMessage(msg))); +} + +export { + print, + error, + warning, + info, + verbose, +}; \ No newline at end of file diff --git a/src/settings.json b/src/settings.json new file mode 100644 index 0000000..cb4d2dc --- /dev/null +++ b/src/settings.json @@ -0,0 +1,50 @@ +{ + "service": { + "name": "Registry Auto Updater" + }, + "showNotification": false, + "updateSets": [ + { + "id": "Interactive logon: Do not require CTRL+ALT+DEL", + "trigger": "Value Change", + "values": [ + { + "key": "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + "name": "DisableCAD", + "value": 1, + "type": "REG_DWORD" + }, + { + "key": "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", + "name": "DisableCAD", + "value": 1, + "type": "REG_DWORD" + } + ] + }, + { + "id": "Enable password manager in Chrome", + "trigger": "Value Change", + "values": [ + { + "key": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Google\\Chrome", + "name": "PasswordManagerEnabled", + "value": 1, + "type": "REG_DWORD" + } + ] + }, + { + "id": "Enable password manager in Edge", + "trigger": "Value Change", + "values": [ + { + "key": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Edge", + "name": "PasswordManagerEnabled", + "value": 1, + "type": "REG_DWORD" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/uninstall-service.js b/src/uninstall-service.js new file mode 100644 index 0000000..208b36f --- /dev/null +++ b/src/uninstall-service.js @@ -0,0 +1,27 @@ +import * as url from 'url'; +import path from 'path'; +import { Service } from 'node-windows'; + +import { + info, + verbose } from './print.js'; +import settings from './settings.json' assert {type: 'json'}; + +main(); + +function main() { + // Create a new service object. + const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + const svc = new Service({ + name: settings.service?.name ?? 'Registry Auto Updater', + script: `${path.join(__dirname, 'app.js')}`, + }); + + // Listen for the "uninstall" event so we know when it's done. + svc.on('uninstall', () => { + verbose('Service uninstalled.'); + }); + + info('Uninstalling service, please accept UAC prompts if any...'); + svc.uninstall(); +} diff --git a/src/windows-registry.js b/src/windows-registry.js new file mode 100644 index 0000000..9902dc7 --- /dev/null +++ b/src/windows-registry.js @@ -0,0 +1,1356 @@ +import ref from 'ref-napi'; +import ffi from 'ffi-napi'; + +import { + error, + warning, + info, + verbose } from './print.js'; +import { + toNullTerminatedWString, + fromWString } from './wstring.js'; +import getWindowsSystemErrorText from './windows-system-error.js'; + +/** Root keys in Windows registry. */ +const RootKey = { + HKEY_CLASSES_ROOT: 0x80000000, + HKCR: 0x80000000, + HKEY_CURRENT_USER: 0x80000001, + HKCU: 0x80000001, + HKEY_LOCAL_MACHINE: 0x80000002, + HKLM: 0x80000002, + HKEY_USERS: 0x80000003, + HKU: 0x80000003, + HKEY_PERFORMANCE_DATA: 0x80000004, + HKEY_PERFORMANCE_TEXT: 0x80000050, + HKEY_PERFORMANCE_NLSTEXT: 0x80000060, + HKEY_CURRENT_CONFIG: 0x80000005, + HKCC: 0x80000005, + HKEY_DYN_DATA: 0x80000006, +}; + +/** Windows registry value types. */ +const RegistryValueType = { + REG_NONE: 0, + REG_SZ: 1, + REG_EXPAND_SZ: 2, + REG_BINARY: 3, + REG_DWORD_LITTLE_ENDIAN: 4, + REG_DWORD: 4, + REG_DWORD_BIG_ENDIAN: 5, + REG_LINK: 6, + REG_MULTI_SZ: 7, + REG_RESOURCE_LIST: 8, + REG_FULL_RESOURCE_DESCRIPTOR: 9, + REG_RESOURCE_REQUIREMENTS_LIST: 10, + REG_QWORD_LITTLE_ENDIAN: 11, + REG_QWORD: 11, +}; + +/** Windows registry access rights used by RegOpenKeyEx and RegCreateKeyEx APIs. */ +const RegistryKeyAccessRight = { + STANDARD_RIGHTS_READ: 0x20000, + STANDARD_RIGHTS_WRITE: 0x20000, + STANDARD_RIGHTS_EXECUTE: 0x20000, + + KEY_QUERY_VALUE: 0x1, + KEY_SET_VALUE: 0x2, + KEY_CREATE_SUB_KEY: 0x4, + KEY_ENUMERATE_SUB_KEYS: 0x8, + KEY_NOTIFY: 0x10, + KEY_CREATE_LINK: 0x20, + + KEY_READ: 0x20019, // Combines the STANDARD_RIGHTS_READ, KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, and KEY_NOTIFY values. + KEY_WRITE: 0x20006, // Combines the STANDARD_RIGHTS_WRITE, KEY_SET_VALUE, and KEY_CREATE_SUB_KEY access rights. + KEY_EXECUTE: 0x20019, // Equivalent to KEY_READ. + KEY_ALL_ACCESS: 0xF003F, // Combines the STANDARD_RIGHTS_REQUIRED, KEY_QUERY_VALUE, KEY_SET_VALUE, KEY_CREATE_SUB_KEY, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY, and KEY_CREATE_LINK access rights. + + KEY_WOW64_32KEY: 0x0200, + KEY_WOW64_64KEY: 0x0100, +}; + +/** Windows registry key creation options used by RegCreateKeyEx API. */ +const RegistryKeyCreateOption = { + REG_OPTION_BACKUP_RESTORE: 0x4, + REG_OPTION_CREATE_LINK: 0x2, + REG_OPTION_NON_VOLATILE: 0x0, + REG_OPTION_VOLATILE: 0x1, +}; + +/** Windows registry notification filters used by RegNotifyChangeKeyValue API. */ +const RegistryKeyNotifyFilter = { + REG_NOTIFY_CHANGE_NAME: 0x1, + REG_NOTIFY_CHANGE_ATTRIBUTES: 0x2, + REG_NOTIFY_CHANGE_LAST_SET: 0x4, + REG_NOTIFY_CHANGE_SECURITY: 0x8, + REG_NOTIFY_THREAD_AGNOSTIC: 0x10000000, +}; + +/** Results returned from WaitForMultipleObjects API. */ +const WaitForMultipleObjectsResult = { + WAIT_OBJECT_0: 0x0, + WAIT_ABANDONED_0: 0x80, + WAIT_TIMEOUT: 0x102, + WAIT_FAILED: 0xFFFFFFFF, +}; + +/** Various error codes returned by APIs that we care about. */ +const ErrorCode = { + KeyNotFound: 2, + KeyMarkedForDeletion: 1018, +} + +// Default interval for the timer used to check for monitor notification. +const DefaultMonitorCheckInterval = 100; + +/** + Define registry and event Windows APIs. + Note, for Unicode version APIs, string parameters defined in ANSI version are replaced with pointer so buffers representing WString can be used since ref.types.CString doesn't handle wide char. + */ +const RegistryApi = ffi.Library('advapi32', { + /* + LSTATUS RegOpenKeyExA( + [in] HKEY hKey, + [in, optional] LPCSTR lpSubKey, + [in] DWORD ulOptions, + [in] REGSAM samDesired, + [out] PHKEY phkResult + ); + + Windows Data Type: + typedef HANDLE HKEY; + typedef PVOID HANDLE; + typedef __nullterminated CONST CHAR *LPCSTR; + typedef char CHAR; + typedef unsigned long DWORD; + */ + 'RegOpenKeyExA': [ 'int', [ 'uint', 'string', 'uint', 'uint', 'pointer' ] ], // Use uint instead of pointer for HKEY hKey as we directly define root key handles' values + + /* + LSTATUS RegOpenKeyExW( + [in] HKEY hKey, + [in, optional] LPCWSTR lpSubKey, + [in] DWORD ulOptions, + [in] REGSAM samDesired, + [out] PHKEY phkResult + ); + + Windows Data Type: + typedef CONST WCHAR *LPCWSTR; + typedef wchar_t WCHAR; + */ + 'RegOpenKeyExW': [ 'int', [ 'uint', 'pointer', 'uint', 'uint', 'pointer' ] ], // Use uint instead of pointer for HKEY hKey as we directly define root key handles' values, also use pointer instead of string for LPCWSTR lpSubKey + + /* + LSTATUS RegCreateKeyExA( + [in] HKEY hKey, + [in] LPCSTR lpSubKey, + DWORD Reserved, + [in, optional] LPSTR lpClass, + [in] DWORD dwOptions, + [in] REGSAM samDesired, + [in, optional] const LPSECURITY_ATTRIBUTES lpSecurityAttributes, + [out] PHKEY phkResult, + [out, optional] LPDWORD lpdwDisposition + ); + + Windows Data Type: + typedef CHAR *LPSTR; + */ + 'RegCreateKeyExA': [ 'int', [ 'uint', 'string', 'uint', 'string', 'uint', 'uint', 'pointer', 'pointer', 'pointer' ] ], // Use uint instead of pointer for HKEY hKey as we directly define root key handles' values + + /* + LSTATUS RegCreateKeyExW( + [in] HKEY hKey, + [in] LPCWSTR lpSubKey, + DWORD Reserved, + [in, optional] LPWSTR lpClass, + [in] DWORD dwOptions, + [in] REGSAM samDesired, + [in, optional] const LPSECURITY_ATTRIBUTES lpSecurityAttributes, + [out] PHKEY phkResult, + [out, optional] LPDWORD lpdwDisposition + ); + */ + 'RegCreateKeyExW': [ 'int', [ 'uint', 'pointer', 'uint', 'pointer', 'uint', 'uint', 'pointer', 'pointer', 'pointer' ] ], // Use uint instead of pointer for HKEY hKey as we directly define root key handles' values, also use pointer instead of string for LPCWSTR lpSubKey and LPWSTR lpClass + + /* + LSTATUS RegQueryValueExA( + [in] HKEY hKey, + [in, optional] LPCSTR lpValueName, + LPDWORD lpReserved, + [out, optional] LPDWORD lpType, + [out, optional] LPBYTE lpData, + [in, out, optional] LPDWORD lpcbData + ); + + Windows Data Type: + typedef unsigned char BYTE; + */ + 'RegQueryValueExA': [ 'int', [ 'pointer', 'string', 'pointer', 'pointer', 'pointer', 'pointer' ] ], + + /* + LSTATUS RegQueryValueExW( + [in] HKEY hKey, + [in, optional] LPCWSTR lpValueName, + LPDWORD lpReserved, + [out, optional] LPDWORD lpType, + [out, optional] LPBYTE lpData, + [in, out, optional] LPDWORD lpcbData + ); + */ + 'RegQueryValueExW': [ 'int', [ 'pointer', 'pointer', 'pointer', 'pointer', 'pointer', 'pointer' ] ], // Use pointer instead of string for LPCWSTR lpValueName + + /* + LSTATUS RegSetValueExA( + [in] HKEY hKey, + [in, optional] LPCSTR lpValueName, + DWORD Reserved, + [in] DWORD dwType, + [in] const BYTE *lpData, + [in] DWORD cbData + ); + */ + 'RegSetValueExA': [ 'int', [ 'pointer', 'string', 'uint', 'uint', 'pointer', 'uint' ] ], + + /* + LSTATUS RegSetValueExW( + [in] HKEY hKey, + [in, optional] LPCWSTR lpValueName, + DWORD Reserved, + [in] DWORD dwType, + [in] const BYTE *lpData, + [in] DWORD cbData + ); + */ + 'RegSetValueExW': [ 'int', [ 'pointer', 'pointer', 'uint', 'uint', 'pointer', 'uint' ] ], // Use pointer instead of string for LPCWSTR lpValueName + + /* + LSTATUS RegNotifyChangeKeyValue( + [in] HKEY hKey, + [in] BOOL bWatchSubtree, + [in] DWORD dwNotifyFilter, + [in, optional] HANDLE hEvent, + [in] BOOL fAsynchronous + ); + + Windows Data Type: + typedef int BOOL; + */ + 'RegNotifyChangeKeyValue': [ 'int', [ 'pointer', 'int', 'uint', 'pointer', 'int' ] ], + + /* + LSTATUS RegCloseKey( + [in] HKEY hKey + ); + */ + 'RegCloseKey': [ 'int', [ 'pointer' ] ], +}); + +const EventApi = ffi.Library('kernel32', { + /* + HANDLE CreateEventA( + [in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes, + [in] BOOL bManualReset, + [in] BOOL bInitialState, + [in, optional] LPCSTR lpName + ); + */ + 'CreateEventA': [ 'pointer', [ 'pointer', 'int', 'int', 'string' ] ], + + /* + HANDLE CreateEventW( + [in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes, + [in] BOOL bManualReset, + [in] BOOL bInitialState, + [in, optional] LPCWSTR lpName + ); + */ + 'CreateEventW': [ 'pointer', [ 'pointer', 'int', 'int', 'pointer' ] ], // Use pointer instead of string for LPCWSTR lpName + + /* + DWORD WaitForSingleObject( + [in] HANDLE hHandle, + [in] DWORD dwMilliseconds + ); + */ + 'WaitForSingleObject': [ 'uint', [ 'pointer', 'uint' ] ], + + /* + DWORD WaitForMultipleObjects( + [in] DWORD nCount, + [in] const HANDLE *lpHandles, + [in] BOOL bWaitAll, + [in] DWORD dwMilliseconds + ); + */ + 'WaitForMultipleObjects': [ 'uint', [ 'uint', 'pointer', 'int', 'uint' ] ], + + /* + BOOL CloseHandle( + [in] HANDLE hObject + ); + */ + 'CloseHandle': [ 'int', [ 'pointer' ] ], +}); + +/** Finalizer for RegistryKey to ensure the underlying registry key handle is closed. */ +const RegistryKeyFinalizer = new FinalizationRegistry(registryKeyData => { + if (registryKeyData.handle !== null) { + warning(`RegistryKey ${registryKeyData.path} is not properly closed! You should call RegistryKey.close() or Registry.closeKey() to close a key when it is no longer needed!`); + Registry.instance.closeKeyInternal(registryKeyData); + } +}); + +/** Finalizer for MonitoredRegistryKey to ensure the underlying event handle is closed. */ +const MonitoredRegistryKeyFinalizer = new FinalizationRegistry(monitorData => { + if (monitorData.waitHandle !== null) { + warning(`MonitoredRegistryKey ${registryKeyData.path} is not properly stopped! You should call Registry.stopMonitor() to stop monitoring a key when it is no longer needed!`); + Registry.instance.stopMonitoredKeyInternal(monitorData); + } +}); + +/** Finalizer for MonitorToken to ensure the callback is unregistered. */ +const MonitorTokenFinalizer = new FinalizationRegistry(tokenData => { + if (!tokenData.calback !== null) { + warning(`MonitorToken on key ${tokenData.path} is not properly removed! You should call MonitorToken.stop() or Registry.stopMonitor() to stop monitoring a key when it is no longer needed!`); + Registry.instance.stopMonitorInternal(tokenData); + } +}); + +/** + * Prints a Windows error message in console. + * @param {string} errorMsg - The message to be printed. + * @param {integer} errorCode - Windows system error code as defined at https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes. + */ +function printWindowsError(errorMsg, errorCode) { + error(`${errorMsg}\r\nError code ${errorCode}: ${getWindowsSystemErrorText(errorCode)}`); +} + +/** Representing a registry key in Windows Registry. */ +class RegistryKey { + /** + * Constructor. Should not be used directly, but use Registry.openKey() to create one. + * @param {string} path - The registry key path. + * @param {boolean} createIfNeeded - (Optional) Whether to create the key if it doesn't exist. Note that it requires appropriate privileges to be able to create key(s). + * If not provided, default value false is used. + * @param {boolean} readonly - (Optional) Whether the handle is used for readonly operation(s) which requires less privilege, or read-write operation(s), which requires more privileges. + * The value is ignored and hardcoded to be false if createIfNeeded is true. + * If not provided and createIfNeeded is false, default value false is used. + */ + constructor(path, createIfNeeded = false, readonly = true) { + this.keyData = { + path: path, + handle: null, + }; + + this.createIfNeeded = createIfNeeded; + if (createIfNeeded && readonly) { + readonly = false; + } + this.readonly = readonly; + + this.open(); + + // Register this key in finalizer so we can be alerted if it's not properly closed + RegistryKeyFinalizer.register(this, this.keyData); + } + + /** + * @return {object} Predefined registry root keys. + */ + static get Root() { + return RootKey; + } + + /** + * @return {boolean} Whether this key is valid. + */ + get isValid() { + return this.keyData.handle !== null; + } + + /** + * Opens (i.e. obtains) handle to the key. + * Note: make sure to call close() when this key is no longer needed. Otherwise the finalizer will complain (though, the finalizer will still properly close the key handle). + * @return {boolean} Whether the handle is opened. Note: the handle may have already been opened previously, in which case this method still returns true. + */ + open() { + if (this.keyData.handle !== null) { + // Already opened + return true; + } + + const { rootKey, subKey } = this.parsePath(); + if (rootKey !== null) { + const accessRight = this.readonly ? RegistryKeyAccessRight.KEY_READ : RegistryKeyAccessRight.KEY_READ | RegistryKeyAccessRight.KEY_WRITE; + const hkeyHandle = ref.alloc('pointer'); + const result = RegistryApi.RegOpenKeyExW(RootKey[rootKey], subKey, 0, accessRight, hkeyHandle); + if (result === 0) { + this.keyData.handle = hkeyHandle.deref(); + } else if (result === ErrorCode.KeyNotFound) { + return this.create(); + } else { + printWindowsError(`Cannot open key "${this.keyData.path}" for ${this.readonly ? 'reading' : 'read-writing'}!`, result); + return false; + } + } else { + error(`Invalid key "${this.keyData.path}". "${rootKey}" is not a predefined Windows registry root key!`); + return false; + } + + return true; + } + + /** + * Reopens the key. + * @return {boolean} Whether the handle is opened. + */ + reopen() { + if (this.close()) { + return this.open(); + } else { + error(`Cannot reopen key "${this.keyData.path}" as previous handle cannot be closed!`); + } + + return false; + } + + /** + * Creates the key. Note, it requires appropriate privileges to be able to create key. + * @return {boolean} Whether the handle is opened. + */ + create() { + if (!this.createIfNeeded) { + // Not allowed to create key + warning(`Cannot create key: "${this.keyData.path}" as createIfNeeded is not set`); + return false; + } + + if (this.keyData.handle !== null) { + // Already opened. Close so we can try creating teh key + if (!this.close()) { + return false; + } + } + + const { rootKey, subKey } = this.parsePath(); + if (rootKey !== null) { + //verbose(`Creating key: ${subKey} under ${rootKey}...`); + const accessRight = RegistryKeyAccessRight.KEY_READ | RegistryKeyAccessRight.KEY_WRITE; + const hkeyHandle = ref.alloc('pointer'); + const result = RegistryApi.RegCreateKeyExW(RootKey[rootKey], subKey, 0, ref.NULL, RegistryKeyCreateOption.REG_OPTION_NON_VOLATILE, accessRight, ref.NULL, hkeyHandle, ref.NULL); + if (result === 0) { + info(`Key "${this.keyData.path}" didn't exist. Created.`); + this.keyData.handle = hkeyHandle.deref(); + } else { + printWindowsError(`Cannot create key "${this.keyData.path}"!`, result); + return false; + } + } + + return true; + } + + /** + * Closes the key's handle. + * @return {boolean} Whether the handle is closed or not. Note: the handle may have already been closed previously, in which case this method still returns true. + */ + close() { + if (this.keyData.handle !== null) { + Registry.instance.closeKeyInternal(this.keyData); + } + return this.keyData.handle === null; + } + + /** + * Retrieves the data for the specified value name. + * @param {string} name - The name of the value to be retrieved. + * @return {any} Retrieved value, which is in a type that depends on the Value Type stored in the registry: + * REG_DWORD/REG_QWORD: unsigned integer + * REG_SZ/REG_EXPAND_SZ: string + * REG_MULTI_SZ: string[] + * REG_BINARY: Buffer + */ + getValue(name) { + let value = null; + if (this.keyData.handle !== null) { + const wstrName = toNullTerminatedWString(name); + const size = ref.alloc('int'); + let result = this.invokeApiWithRetry(() => RegistryApi.RegQueryValueExW(this.keyData.handle, wstrName, ref.NULL, ref.NULL, ref.NULL, size)); + if (result === 0) { + const buffer = Buffer.alloc(size.deref()); + const valueType = ref.alloc('int'); + result = this.invokeApiWithRetry(() => RegistryApi.RegQueryValueExW(this.keyData.handle, wstrName, ref.NULL, valueType, buffer, size)); + if (result === 0) { + //verbose(`ValueType of ${name}: ${valueType.deref()}`); + //verbose(`Length of ${name}: ${buffer.length}`); + switch (valueType.deref()) { + case RegistryValueType.REG_DWORD: + // Value is expected as unsigned integer + value = buffer.readUInt32LE(); + break; + + case RegistryValueType.REG_QWORD: + // Value is expected as unsigned integer + value = buffer.readUInt64LE(); + break; + + case RegistryValueType.REG_SZ: + case RegistryValueType.REG_EXPAND_SZ: + // Value is expected as string + value = fromWString(buffer); + break; + + case RegistryValueType.REG_MULTI_SZ: + // Value is expected as string[] + value = []; + let currentStringStart = 0; + let i = 2; + while (i < buffer.length - 2) { // Ignore the final/extra NULL terminator that ends the whole sequence + // In UTF-16 encoding, a NULL terminator is 2 bytes of zeros + if (buffer[i] === 0 && buffer[i + 1] === 0) { + value.push(fromWString(buffer, currentStringStart, i)); + currentStringStart = i + 2; + i = currentStringStart; + } + i += 2; + } + break; + + case RegistryValueType.REG_BINARY: + // Value is expected as Buffer + value = buffer; + break; + + default: + error(`Value "${name}" of key "${this.keyData.path}" has a type ${valueType.deref()}, which is not supported!`); + return null; + } + } else { + printWindowsError(`Cannot read value "${name}" of key "${this.keyData.path}"!`, result); + } + } else { + printWindowsError(`Cannot read value "${name}" of key "${this.keyData.path}" to find out its size!`, result); + } + } else { + error(`Trying to read value "${name}" of key "${this.keyData.path}" without obtaining a valid handle!`); + } + + return value; + } + + /** + * Sets the data (and optionally type) for the specified value name. + * @param {string} name - The name of the value to be set. + * @param {any} value - The data of the value to be set, which needs to be in a type that corresponds to the type param (if provided), or the Value Type stored in the registry: + * REG_DWORD/REG_QWORD: unsigned integer + * REG_SZ/REG_EXPAND_SZ: string + * REG_MULTI_SZ: string[] + * REG_BINARY: Buffer + * @param {RegistryValueType} type - (Optional) The type of the value to be set. + * If provided, it determines the type to be stored in the registry, and also dictates the type of the value param. + * If not provided, the the Value Type stored in the registry is used. In this case the value has to already exist in the registry. + * @return {boolean} Whether the operation succeeded. + */ + setValue(name, value, type) { + let success = false; + if (this.keyData.handle !== null) { + const wstrName = toNullTerminatedWString(name); + if (!type) { + type = this.getValueType(name); + if (type === RegistryValueType.REG_NONE) { + return false; + } + } + + let size = 0; + let data = null; + switch (type) { + case RegistryValueType.REG_DWORD: + // Value is expected as unsigned integer + size = 4; + data = Buffer.alloc(size); + data.writeUInt32LE(value); + break; + + case RegistryValueType.REG_QWORD: + // Value is expected as unsigned integer + size = 8; + data = Buffer.alloc(size); + data.writeUInt64LE(value); + break; + + case RegistryValueType.REG_SZ: + case RegistryValueType.REG_EXPAND_SZ: + // Value is expected as string + data = toNullTerminatedWString(value); + size = data.length; + break; + + case RegistryValueType.REG_MULTI_SZ: + // Value is expected as string[] + const wstrings = value.map(str => toNullTerminatedWString(str)); + size = wstrings.reduce((sum, wstring) => sum + wstring.length, 0) + 2; // Add the final/extra NULL terminator that ends the sequence + data = Buffer.alloc(size); + let pos = 0; + wstrings.forEach(wstring => { + wstring.copy(data, pos); + pos += wstring.length; + }); + break; + + case RegistryValueType.REG_BINARY: + // Value is expected as Buffer + data = value; + size = data.length; + break; + + default: + error(`Value "${name}" of key "${this.keyData.path}" with type ${valueType.deref()} is not supported!`); + return false; + } + + const result = this.invokeApiWithRetry(() => RegistryApi.RegSetValueExW(this.keyData.handle, wstrName, 0, type, data, size)); + if (result === 0) { + success = true; + } else { + printWindowsError(`Cannot set value "${name}" of key "${this.keyData.path}"!`, result); + } + } else { + error(`Trying to set value "${name}" of key "${this.keyData.path}" without obtaining a valid handle!`); + } + + return success; + } + + /** + * Retrieves the type of the data for the specified value name. + * @param {string} name - The name of the value. + * @return {RegistryValueType} Retrieved value data type. + */ + getValueType(name) { + if (this.keyData.handle !== null) { + const wstrName = toNullTerminatedWString(name); + const valueType = ref.alloc('int'); + const result = this.invokeApiWithRetry(() => RegistryApi.RegQueryValueExW(this.keyData.handle, wstrName, ref.NULL, valueType, ref.NULL, ref.NULL)); + if (result === 0) { + return valueType.deref(); + } else { + printWindowsError(`Cannot read value "${name}" of key "${this.keyData.path}" to find out its type!`, result); + } + } else { + error(`Trying to get value type of "${name}" of key "${this.keyData.path}" without obtaining a valid handle!`); + } + + return RegistryValueType.REG_NONE; + } + + /** + * Checks whether a value exists with the specified value name. + * @param {string} name - The name of the value to be checked. + * @return {boolean} Whether the specified value exists + */ + checkValueExistence(name) { + if (this.keyData.handle !== null) { + const wstrName = toNullTerminatedWString(name); + const result = this.invokeApiWithRetry(() => RegistryApi.RegQueryValueExW(this.keyData.handle, wstrName, ref.NULL, ref.NULL, ref.NULL, ref.NULL)); + if (result === 0) { + return true; + } else if (result === ErrorCode.KeyNotFound) { + return false; + } else { + printWindowsError(`Cannot check the existence of "${name}" of key "${this.keyData.path}"!`, result); + } + } else { + error(`Trying to check the existence of "${name}" of key "${this.keyData.path}" without obtaining a valid handle!`); + } + + return false; + } + + /** Private method: parses key path. */ + parsePath() { + const pathParts = this.keyData.path.split('\\'); + let rootKey = pathParts[0].toUpperCase(); + if (rootKey.endsWith(':')) { + // Handle the case when it is in a format like HKLM:\subkey + rootKey = rootKey.slice(0, -1); + } + + if (RootKey.hasOwnProperty(rootKey)) { + return { rootKey: rootKey, subKey: toNullTerminatedWString(pathParts.slice(1).join('\\')) }; + } else { + error(`Invalid key "${this.keyData.path}". "${rootKey}" is not a predefined Windows registry root key!`); + return { rootKey: null, subKey: null }; + } + } + + /** Private method: invokes an API with retries. */ + invokeApiWithRetry(invokeApi, numRetries = 1) { + ++numRetries; // The first try does not count as "retry" + let result = 0; + while (numRetries-- > 0) { + result = invokeApi(); + if (result === ErrorCode.KeyMarkedForDeletion) { + // Key was deleted. Reopen the key and retry + warning(`Key "${this.keyData.path}" was deleted. Trying to reopen the key and execute again...`); + this.reopen(); + } else { + // Success or error that cannot be handled + break; + } + } + + return result; + } +} + +/** Private class. Representing a monitored registry key in Windows Registry. */ +class MonitoredRegistryKey extends RegistryKey { + /** + * Constructor. Should not be used directly, but use Registry.monitorXXX() to create one. + * @param {string} path - The registry key path. + * @param {boolean} recursive - Whether to monitor sub-keys recursively. + * @param {boolean} createIfNeeded - Whether to create the key if it doesn't exist. Note that it requires appropriate privileges to be able to create key(s). + * @param {function} callback - The callback to be added. + */ + constructor(path, recursive, createIfNeeded, callback) { + super(path, createIfNeeded); + this.monitorData = { + path: path, + recursive: recursive, + waitHandle: null, + }; + + this.callbacks = []; + this.addCallback(callback); + this.start(); + + // Register this key in finalizer so we can be alerted if it's not properly stopped + MonitoredRegistryKeyFinalizer.register(this, this.monitorData); + } + + /** + * @return {boolean} Whether this key is valid. + */ + get isValid() { + return super.isValid && this.monitorData.waitHandle !== null; + } + + /** + * Starts monitoring. + * Note: make sure to call stop() when this key is no longer needed to be monitored. Otherwise the finalizer will complain (though, the finalizer will still properly stop the monitor). + * @return {boolean} Whether the monitor is started. Note: the monitor may have already been started previously, in which case this method still returns true. + */ + start() { + if (this.monitorData.waitHandle !== null) { + // Already started + return true; + } + + if (super.isValid) { + this.monitorData.waitHandle = EventApi.CreateEventW( + ref.NULL, // Use default security descriptor + 0, // Use auto-reset behavior + 0, // Initial state is unset + ref.NULL // No need to assign a name as Registry class is making sure there is only one monitor per key path + ); + if (ref.isNull(this.monitorData.waitHandle)) { + this.monitorData.waitHandle = null; + error(`Cannot create wait handle to monitor key "${this.keyData.path}"`); + return false; + } + + return this.registerForNotification(); + } + + return false; + } + + /** + * Stops monitoring. + * @return {boolean} Whether the monitor is stopped. Note: the monitor may have already been stopped previously, in which case this method still returns true. + */ + stop() { + if (this.monitorData.waitHandle === null) { + // Already stopped + this.callbacks = []; + return true; + } + + Registry.instance.stopMonitoredKeyInternal(this.monitorData); + if (this.monitorData.waitHandle === null) { + this.callbacks = []; + return true; + } + + return false; + } + + /** + * Adds a callback. + * @param {function} callback - The callback to be added. It will receive this MonitoredRegistryKey as parameter. + */ + addCallback(callback) { + if (callback) { + this.callbacks.push(callback); + } + } + + /** + * Removes a callback. + * @param {function} callback - The callback to be removed. + * @return {boolean} Whether the operation is successful. + */ + removeCallback(callback) { + const index = this.callbacks.indexOf(callback); + if (index >= 0) { + this.callbacks.splice(index, 1); + + if (this.callbacks.length === 0) { + // No need to monitor this key anymore + return this.stop() && this.close(); + } + + return true; + } else { + return false; + } + } + + /** + * Reopens the key. + * @return {boolean} Whether the handle is opened. + */ + reopen() { + if (super.reopen()) { + if (this.isValid) { + // Re-register for notification since the underlying key handle is changed. + // Note, the same wait handle is reused as there is no need to change it, and changing it also means Registry.updateMonitoredKeysArray() needs to be invoked + return this.registerForNotification(); + } + } + + return false; + } + + /** + * Called when monitored key triggers. + */ + onMonitorTriggered() { + // Register for change notification again (RegNotifyChangeKeyValue only triggers once) + this.registerForNotification(); + + // Notify all clients + this.callbacks.forEach((callback) => callback(this)); + } + + /** Private method: registers for notification from change event on the key. */ + registerForNotification() { + const result = RegistryApi.RegNotifyChangeKeyValue( + this.keyData.handle, + this.monitorData.recursive ? 1 : 0, + this.monitorData.recursive ? (RegistryKeyNotifyFilter.REG_NOTIFY_CHANGE_LAST_SET | RegistryKeyNotifyFilter.REG_NOTIFY_CHANGE_NAME) : RegistryKeyNotifyFilter.REG_NOTIFY_CHANGE_LAST_SET, + this.monitorData.waitHandle, + 1 // Use async behavior + ); + if (result === ErrorCode.KeyMarkedForDeletion) { + // Key was deleted. Reopen the key, which would call this method again + warning(`Key "${this.monitorData.path}" was deleted. Trying to reopen the key and register for notification again...`); + this.reopen(); + } else if (result !== 0) { + printWindowsError(`Cannot register for notification on key "${this.monitorData.path}"!`, result); + return false; + } else { + return true; + } + } +} + +/** Representing a token that can be used to stop monitoring later on. */ +class MonitorToken { + /** + * Constructor. Should not be used directly, but use Registry.monitorKey() to create one. + * @param {string} path - The registry key path. + * @param {boolean} recursive - Whether to monitor sub-keys recursively. + * @param {function} callback - The callback function to be invoked when changes in the key happen. + */ + constructor(path, recursive, callback) { + this.tokenData = { + path: path, + recursive: recursive, + callback: callback, + }; + + // Register this token in finalizer so we can be alerted if it's not properly stopped + MonitorTokenFinalizer.register(this, this.tokenData); + } + + /** + * Stops monitoring. + * @return {boolean} Whether the callback is stopped. Note: the callback may have already been stopped previously, in which case this method still returns true. + */ + stop() { + Registry.instance.stopMonitorInternal(this.tokenData); + return this.tokenData.callback === null; + } +} + +/** Representing a Windows Registry object. */ +class Registry { + static instance; + static instanceCreated; + static { + this.instance = new Registry(); + this.instanceCreated = true; + } + + /** + * Private constructor. + */ + constructor() { + if (Registry.instanceCreated) { + throw new Error("Registry is using singleton pattern. Please use Registry.instance to access."); + } + + this.monitoredKeys = {}; + this.monitoredRecursiveKeys = {}; + this.checkInterval = DefaultMonitorCheckInterval; + this.checkTimer = null; + } + + /** + * @return {object} Registry value types. + */ + static get ValueType() { + return RegistryValueType; + } + + /** + * Sets monitor check interval. + * @param {integer} interval - Monitor check interval in millisecond. + */ + set monitorCheckInterval(interval) { + this.checkInterval = interval; + if (this.checkTimer !== null) { + this.stopMonitorTimer(); + this.startMonitorTimer(); + } + } + + /** + * Obtains a handle to a registry key. + * Note: make sure the returned RegistryKey object is closed when it is no longer needed. Otherwise the finalizer will complain (though, the finalizer will still properly close the key handle). + * @param {string} path - The registry key path. + * @param {boolean} createIfNeeded - (Optional) Whether to create the key if it doesn't exist. Note that it requires appropriate privileges to be able to create key(s). + * If not provided, default value false is used. + * @param {boolean} readonly - (Optional) Whether the handle is used for readonly operation(s) which requires less privilege, or read-write operation(s), which requires more privileges. + * The value is ignored and hardcoded to be false if createIfNeeded is true. + * If not provided and createIfNeeded is false, default value false is used. + * @return {RegistryKey} Obtained key handle wrapped in a RegistryKey. + */ + openKey(path, createIfNeeded = false, readonly = true) { + const key = new RegistryKey(path, createIfNeeded, readonly); + if (key.isValid) { + return key; + } + + return null; + } + + /** + * Closes a key handle. + * @param {RegistryKey} key - The key handle to be closed. + * @return {boolean} Whether the handle is closed or not. Note: the handle may have already been closed previously, in which case this method still returns true. + */ + closeKey(key) { + return this.closeKeyInternal(key.keyData); + } + + /** Private method: closes a key using its internal data structure. */ + closeKeyInternal(keyData) { + if (keyData.handle !== null) { + const result = RegistryApi.RegCloseKey(keyData.handle); + if (result !== 0) { + printWindowsError(`Cannot close key "${keyData.path}}"!`, result); + return false; + } else { + keyData.handle = null; + //verbose(`Closed key "${keyData.path}}"`); + } + } else { + warning(`Key "${keyData.path}}" was already closed.`); + } + + return true; + } + + /** + * Retrieves the data for the specified key path and value name. + * @param {string} path - The registry key path. + * @param {string} name - The name of the value to be retrieved. + * @return {any} Retrieved value, which is in a type that depends on the Value Type stored in the registry: + * REG_DWORD/REG_QWORD: integer + * REG_SZ/REG_EXPAND_SZ: string + * REG_MULTI_SZ: string[] + * REG_BINARY: Buffer + */ + getValue(path, name) { + let value = null; + const key = this.openKey(path); + if (key !== null) { + value = key.getValue(name); + this.closeKey(key); + } + return value; + } + + /** + * Sets the data (and optionally type) for the specified key path and value name. + * @param {string} path - The registry key path. + * @param {string} name - The name of the value to be set. + * @param {any} value - The data of the value to be set, which needs to be in a type that corresponds to the type param (if provided), or the Value Type stored in the registry: + * REG_DWORD/REG_QWORD: integer + * REG_SZ/REG_EXPAND_SZ: string + * REG_MULTI_SZ: string[] + * REG_BINARY: Buffer + * @param {RegistryValueType} type - (Optional) The type of the value to be set. + * If provided, it determines the type to be stored in the registry, and also dictates the type of the value param. + * If not provided, the the Value Type stored in the registry is used. In this case the value has to already exist in the registry. + * @return {boolean} Whether the operation succeeded. + */ + setValue(path, name, value, type) { + let success = false; + const key = this.openKey(path, true, false); + if (key !== null) { + success = key.setValue(name, value, type); + this.closeKey(key); + } + return success; + } + + /** + * Retrieves the type of the data for the specified key path and value name. + * @param {string} path - The registry key path. + * @param {string} name - The name of the value. + * @return {RegistryValueType} Retrieved value data type. + */ + getValueType(path, name) { + let valueType = RegistryValueType.REG_NONE; + const key = this.openKey(path); + if (key !== null) { + valueType = key.getValueType(name); + this.closeKey(key); + } + return valueType; + } + + /** + * Checks whether a value exists with the specified key path and value name. + * @param {string} path - The registry key path. + * @param {string} name - The name of the value to be checked. + * @return {boolean} Whether the specified value exists + */ + checkValueExistence(path, name) { + let exists = false; + const key = this.openKey(path); + if (key !== null) { + exists = key.checkValueExistence(name); + this.closeKey(key); + } + return exists; + } + + /** + * Starts monitoring a key for any value changes under the key. Sub-keys/sub-tree changes are not supported. + * Note: make sure to call stopMonitor() when this key is no longer needed to be monitored. Otherwise the finalizer will complain (though, the finalizer will still properly stop the monitor). + * @param {string} path - The registry key path. + * @param {boolean} recursive - Whether to monitor sub-keys recursively. + * @param {boolean} createIfNeeded - Whether to create the key if it doesn't exist. Note that it requires appropriate privileges to be able to create key(s). + * @param {function} callback - The callback when change happens. It will receive MonitoredRegistryKey instance as parameter. + * @return {MonitorToken} A token that can used later on to stop monitoring. If the operation fails, null is returned. + */ + monitorKey(path, recursive, createIfNeeded, callback) { + // Check if the key's path is already monitored + let key = recursive ? this.monitoredRecursiveKeys[path] : this.monitoredKeys[path]; + if (key) { + if (key.isValid) { + key.addCallback(callback); + } else { + warning(`Currently monitored key "${path}" is no longer valid. Creating a new one to replace.`); + key = null; + } + } + + if (!key) { + key = new MonitoredRegistryKey(path, recursive, createIfNeeded, callback); + if (key.isValid) { + // Add this key's path in map + if (recursive) { + this.monitoredRecursiveKeys[path] = key; + } else { + this.monitoredKeys[path] = key; + } + + // And also update the handle array used in WaitForMultipleObjects call + this.updateMonitoredKeysArray(); + } else { + key.stop(); + key = null; + } + } + + if (key) { + this.startMonitorTimer(); + return new MonitorToken(path, recursive, callback); + } + + return null; + } + + /** + * Starts monitoring a value for any value changes that make it different than compare value. + * Note: make sure to call stopMonitor() when this key is no longer needed to be monitored. Otherwise the finalizer will complain (though, the finalizer will still properly stop the monitor). + * @param {string} path - The registry key path. + * @param {string} name - The name of the value to be set. + * @param {string} compareValue - The value to be compared with. If null is passed in, any value change would trigger the callback. + * @param {boolean} createIfNeeded - Whether to create the key if it doesn't exist. Note that it requires appropriate privileges to be able to create key(s). + * @param {function} callback - The callback when change happens. It will receive the monitored key itself, plus current and compare value as parameter. + * @return {MonitorToken} A token that can used later on to stop monitoring. If the operation fails, null is returned. + */ + monitorValue(path, name, compareValue, createIfNeeded, callback) { + const getValueType = ((monitorToken, monitoredKey) => { + // Get value type + monitorToken.valueType = monitoredKey.getValueType(name); + if (monitorToken.valueType === RegistryValueType.REG_NONE) { + // Cannot get value type from name + this.stopMonitor(monitorToken); + monitorToken = null; + return false; + } else if (monitorToken.valueType !== RegistryValueType.REG_DWORD && + monitorToken.valueType !== RegistryValueType.REG_QWORD && + monitorToken.valueType !== RegistryValueType.REG_SZ && + monitorToken.valueType !== RegistryValueType.REG_EXPAND_SZ && + monitorToken.valueType !== RegistryValueType.REG_MULTI_SZ && + monitorToken.valueType !== RegistryValueType.REG_BINARY) { + throw new Error(`Value "${name}" of key "${path}" with type ${monitorToken.valueType} is not supported!`); + } + + return true; + }); + + const monitorToken = this.monitorKey(path, false, createIfNeeded, (monitoredKey) => { + let valueChanged = false; + const currentValue = monitoredKey.getValue(name); + if (!monitorToken.valueExists) { + if (currentValue !== null) { + monitorToken.valueExists = true; + valueChanged = true; + + // Value created, get the type and update compareValue if needed + if (getValueType(monitorToken, monitoredKey) && monitorToken.trackCurrentValue) { + compareValue = currentValue; + } + } + } else { + valueChanged = !this.checkValue(currentValue, compareValue, monitorToken.valueType); + } + + if (valueChanged) { + callback(monitoredKey, currentValue, compareValue); + if (monitorToken.trackCurrentValue) { + compareValue = currentValue; + } + } + }); + + if (monitorToken !== null) { + monitorToken.trackCurrentValue = (compareValue === null); + + // Check value existence + const monitoredKey = this.monitoredKeys[path]; + monitorToken.valueExists = monitoredKey.checkValueExistence(name); + if (monitorToken.valueExists && getValueType(monitorToken, monitoredKey)) { + if (monitorToken.trackCurrentValue) { + compareValue = monitoredKey.getValue(name); + if (compareValue === null) { + // Cannot read value from name + this.stopMonitor(monitorToken); + monitorToken = null; + } + } else { + // Perform initial check to make sure current value is the same as compareValue + const currentValue = monitoredKey.getValue(name); + if (!this.checkValue(currentValue, compareValue, monitorToken.valueType)) { + callback(monitoredKey, currentValue, compareValue); + } + } + } + } + + return monitorToken; + } + + /** + * Stops monitoring a key with given token. + * @param {MonitorToken} monitorToken - The monitor token. + * @return {boolean} Whether the handle is closed or not. Note: the handle may have already been closed previously, in which case this method still returns true. + */ + stopMonitor(monitorToken) { + return this.stopMonitorInternal(monitorToken.tokenData); + } + + /** Private method: stops monitoring a key with a given token using its internal data structure. */ + stopMonitorInternal(tokenData) { + if (tokenData.callback !== null) { + const monitoredKey = tokenData.recursive ? this.monitoredRecursiveKeys[tokenData.path] : this.monitoredKeys[tokenData.path]; + if (monitoredKey) { + if (!monitoredKey.removeCallback(tokenData.callback)) { + return false; + } + //verbose(`Stopped monitoring key "${tokenData.path}}"`); + } else { + warning(`It seems key "${tokenData.path}}" is not being monitored.`); + } + + tokenData.callback = null; + } else { + warning(`Monitored token with key "${tokenData.path}}" was already stopped.`); + } + + return true; + } + + /** Private method: stops monitoring a key using its internal data structure. */ + stopMonitoredKeyInternal(monitorData) { + if (monitorData.waitHandle !== null) { + const result = EventApi.CloseHandle(monitorData.waitHandle); + if (result === 0) { + error(`Cannot close wait handle for monitored key "${this.keyData.path}"`); + return false; + } else { + monitorData.waitHandle = null; + + // Remove it from monitored keys map. + if (monitorData.recursive) { + delete this.monitoredRecursiveKeys[monitorData.path]; + } else { + delete this.monitoredKeys[monitorData.path]; + } + this.updateMonitoredKeysArray(); + //verbose(`Monitored key "${keyData.path}}" stopped`); + } + } else { + warning(`MonitoredKey "${keyData.path}}" was already stopped.`); + } + + return true; + } + + /** Private method: starts monitor timer. */ + startMonitorTimer() { + if (this.checkTimer === null) { + this.checkTimer = setInterval(this.monitorCheck.bind(this), this.checkInterval); + } + } + + /** Private method: stops monitor timer. */ + stopMonitorTimer() { + if (this.checkTimer !== null) { + clearTimeout(this.checkTimer); + this.checkTimer = null; + } + } + + /** Private method: checks to see if any monitored key notified. */ + monitorCheck() { + while (true) { + const result = EventApi.WaitForMultipleObjects( + this.monitoredKeysArray.length, + this.monitoredKeyHandlesBuffer, + 0, // Wait for any + 0 // Do not wait + ); + + if (result === WaitForMultipleObjectsResult.WAIT_FAILED) { + error('monitorCheck: WaitForMultipleObjects failed!'); + break; + } else if (result === WaitForMultipleObjectsResult.WAIT_TIMEOUT) { + //verbose('monitorCheck: WaitForMultipleObjects timed out. No key has signaled.'); + break; + } else if (result >= WaitForMultipleObjectsResult.WAIT_ABANDONED_0) { + error('monitorCheck: WaitForMultipleObjects reported that handle(s) were abandoned, which should not happen!'); + break; + } else { + // A monitored key has signaled. Trigger its callback and continue wait in case there are other keys that have signaled as well + const monitoredKey = this.monitoredKeysArray[result - WaitForMultipleObjectsResult.WAIT_OBJECT_0]; + monitoredKey.onMonitorTriggered(); + } + } + } + + /** Private method: updates monitored keys array when there are changes to monitored keys. */ + updateMonitoredKeysArray() { + this.monitoredKeysArray = []; + for (const path in this.monitoredRecursiveKeys) { + this.monitoredKeysArray.push(this.monitoredRecursiveKeys[path]); + } + for (const path in this.monitoredKeys) { + this.monitoredKeysArray.push(this.monitoredKeys[path]); + } + + // Also re-creates handle array buffer to be used as lpHandles in WaitForMultipleObjects API. + this.monitoredKeyHandlesBuffer = Buffer.alloc(ref.sizeof.pointer * this.monitoredKeysArray.length); + if (this.monitoredKeysArray.length === 0) { + this.stopMonitorTimer(); + } else { + const copyToBuffer = ref.sizeof.pointer == 4 ? this.monitoredKeyHandlesBuffer.writeUInt32LE : this.monitoredKeyHandlesBuffer.writeUInt64LE; + for (let i = 0; i < this.monitoredKeysArray.length; ++i) { + this.monitoredKeyHandlesBuffer.writeUInt64LE(ref.address(this.monitoredKeysArray[i].monitorData.waitHandle), i * ref.sizeof.pointer); + } + } + } + + /** Private method: compares registry values based on value type. */ + checkValue(currentValue, compareValue, valueType) { + if (currentValue === null && compareValue === null) { + return true; + } else if (currentValue === null || compareValue === null) { + return false; + } + + switch (valueType) { + case RegistryValueType.REG_DWORD: // Value is expected as unsigned integer + case RegistryValueType.REG_QWORD: // Value is expected as unsigned integer + case RegistryValueType.REG_SZ: // Value is expected as string + case RegistryValueType.REG_EXPAND_SZ: // Value is expected as string + return (currentValue === compareValue); + + case RegistryValueType.REG_MULTI_SZ: + // Value is expected as string[] + if (currentValue.length !== compareValue.length) { + return false; + } else { + for (let i = 0; i < currentValue.length; ++i) { + if (currentValue[i] !== compareValue[i]) { + return false; + } + } + } + return true; + + case RegistryValueType.REG_BINARY: + // Value is expected as Buffer + return (currentValue.size === compareValue.size && currentValue.compare(compareValue) === 0); + + default: + throw new Error(`Value "${name}" of key "${path}" with type ${monitorToken.valueType} is not supported!`); + } + } +} + +export { + RegistryKey, + Registry, +}; diff --git a/src/windows-system-error.js b/src/windows-system-error.js new file mode 100644 index 0000000..903e296 --- /dev/null +++ b/src/windows-system-error.js @@ -0,0 +1,134 @@ +import ref from 'ref-napi'; +import ffi from 'ffi-napi'; + +import { fromNullTerminatedWString } from './wstring.js'; + +const Flags = { + FORMAT_MESSAGE_ALLOCATE_BUFFER: 0x00000100, + FORMAT_MESSAGE_ARGUMENT_ARRAY: 0x00002000, + FORMAT_MESSAGE_FROM_HMODULE: 0x00000800, + FORMAT_MESSAGE_FROM_STRING: 0x00000400, + FORMAT_MESSAGE_FROM_SYSTEM: 0x00001000, + FORMAT_MESSAGE_IGNORE_INSERTS: 0x00000200, + FORMAT_MESSAGE_MAX_WIDTH_MASK: 0x000000FF, +}; + +const win32Api = ffi.Library('kernel32', { + /* + DWORD FormatMessageA( + [in] DWORD dwFlags, + [in, optional] LPCVOID lpSource, + [in] DWORD dwMessageId, + [in] DWORD dwLanguageId, + [out] LPSTR lpBuffer, + [in] DWORD nSize, + [in, optional] va_list *Arguments + ); + */ + 'FormatMessageA': [ 'uint', [ 'uint', 'pointer', 'uint', 'uint', 'pointer', 'uint', 'pointer' ] ], + + /* + DWORD FormatMessageW( + [in] DWORD dwFlags, + [in, optional] LPCVOID lpSource, + [in] DWORD dwMessageId, + [in] DWORD dwLanguageId, + [out] LPWSTR lpBuffer, + [in] DWORD nSize, + [in, optional] va_list *Arguments + ); + */ + 'FormatMessageW': [ 'uint', [ 'uint', 'pointer', 'uint', 'uint', 'pointer', 'uint', 'pointer' ] ], + + /* + HLOCAL LocalFree( + [in] _Frees_ptr_opt_ HLOCAL hMem + ); + */ + 'LocalFree': [ 'uint', [ 'pointer' ] ], +}); + +export default function getWindowsSystemErrorText(errorCode) { + if (errorCode > 0) { + const temp = ref.alloc('pointer'); + + // Use ANSI version since FormatMessageW returns number of TCHARs, and we cannot calculate the exactly required number of bytes if wide char is used because UTF-16 is variable length character encoding. + // The wide char version allocates more memory to workaround it, but it's kind of waste of memory. Plus, the language used here is LANG_NEUTRAL anyway. + let size = win32Api.FormatMessageA( + Flags.FORMAT_MESSAGE_FROM_SYSTEM | // Use system message tables to retrieve error text + Flags.FORMAT_MESSAGE_ALLOCATE_BUFFER | // Allocate buffer on local heap for error text + Flags.FORMAT_MESSAGE_IGNORE_INSERTS, // Important! will fail otherwise, since we're not (and CANNOT) pass insertion parameters + ref.NULL, + errorCode, + 0, + temp, + 0, + ref.NULL + ); + if (size > 0) { + // Since we cannot recognize the received message as a string, we need to allocate a buffer and use the right size to receive it again. + // Thus we need to release the buffer allocated by FormatMessage(). + // Note, when FORMAT_MESSAGE_ALLOCATE_BUFFER is used, lpBuffer effectively becomes pointer to pointer of string, i.e. char ** + win32Api.LocalFree(temp.deref()); + + ++size; // Add NULL terminator + const errorText = Buffer.alloc(size); + size = win32Api.FormatMessageA( + Flags.FORMAT_MESSAGE_FROM_SYSTEM | // Use system message tables to retrieve error text + Flags.FORMAT_MESSAGE_IGNORE_INSERTS, // Important! will fail otherwise, since we're not (and CANNOT) pass insertion parameters + ref.NULL, + errorCode, + 0, + errorText, + size, + ref.NULL + ); + + if (size > 0) { + // Remove NULL terminator when converting to JS string. + return errorText.toString('latin1', 0, size - 1); + } + } +/* + let size = win32Api.FormatMessageW( + Flags.FORMAT_MESSAGE_FROM_SYSTEM | // Use system message tables to retrieve error text + Flags.FORMAT_MESSAGE_ALLOCATE_BUFFER | // Allocate buffer on local heap for error text + Flags.FORMAT_MESSAGE_IGNORE_INSERTS, // Important! will fail otherwise, since we're not (and CANNOT) pass insertion parameters + ref.NULL, + errorCode, + 0, + temp, + 0, + ref.NULL + ); + if (size > 0) { + // Since we cannot recognize the received message as a string, we need to allocate a buffer and use the right size to receive it again. + // Thus we need to release the buffer allocated by FormatMessage(). + // Note, when FORMAT_MESSAGE_ALLOCATE_BUFFER is used, lpBuffer effectively becomes pointer to pointer of string, i.e. char ** + win32Api.LocalFree(temp.deref()); + + // UTF-16 is variable length character encoding, and since we only know number of TCHARs, it is not possible to know the exact number of bytes required. + // However, a char in UTF-16 either takes 2 bytes or 4 bytes, so we can calculate the maximally possible number of bytes required. + // Also add NULL terminator at the end, which is 2 bytes of zeros in UTF-16 encoding. + size = size * 4 + 2; + const errorText = Buffer.alloc(size); + size = win32Api.FormatMessageW( + Flags.FORMAT_MESSAGE_FROM_SYSTEM | // Use system message tables to retrieve error text + Flags.FORMAT_MESSAGE_IGNORE_INSERTS, // Important! will fail otherwise, since we're not (and CANNOT) pass insertion parameters + ref.NULL, + errorCode, + 0, + errorText, + size, + ref.NULL + ); + + if (size > 0) { + return fromNullTerminatedWString(errorText); + } + } + */ + } + + return ''; +} diff --git a/src/wstring.js b/src/wstring.js new file mode 100644 index 0000000..3bcc100 --- /dev/null +++ b/src/wstring.js @@ -0,0 +1,72 @@ +const WindowsWideCharEncoding = 'utf16le'; + +/** + * Converts a JavaScript string to WString (wide char encoding). + * @param {string} str - The string containing two comma-separated numbers. + * @return {Buffer} Converted WString in a buffer. When used in ffi, define the parameter type as 'pointer' instead of 'string'. + */ +function toWString(str) { + return Buffer.from(str, WindowsWideCharEncoding); +} + +/** + * Converts a JavaScript string to a NULL-terminated WString (wide char encoding) that can be used for Win32 APIs (xxxxW). + * @param {string} str - The string containing two comma-separated numbers. + * @return {Buffer} Converted WString in a buffer. When used in ffi, define the parameter type as 'pointer' instead of 'string'. + */ +function toNullTerminatedWString(str) { + // Make sure NULL terminator is added. + return Buffer.from(str + '\0', WindowsWideCharEncoding); +} + +/** + * Converts a buffer that contains a WString (wide char encoding) to JavaScript string. + * @param {Buffer} buffer - The buffer that contains the WString to be converted. + * @param {integer} start - (Optional) The byte offset to start converting at. + * Useful if there are data other than a single WString in the buffer, e.g. REG_MULTI_SZ where NULL terminator is used to separate strings. + * If not provided, default value 0 is used (Buffer.toString() behavior). + * @param {integer} end - (Optional) The byte offset to stop converting at (not inclusive). + * Useful if there are data other than a single WString in the buffer, e.g. REG_MULTI_SZ where NULL terminator is used to separate strings. + * If not provided, default value is end of buffer (Buffer.toString() behavior). + * @return {string} Converted JavaScript string. + */ +function fromWString(buffer, start, end) { + return buffer.toString(WindowsWideCharEncoding, start, end); +} + +/** + * Converts a buffer that contains a NULL-terminated WString (wide char encoding) to JavaScript string. + * @param {Buffer} buffer - The buffer that contains the WString to be converted. + * @param {integer} start - (Optional) The byte offset to start converting at. + * Useful if there are data other than a single WString in the buffer, e.g. REG_MULTI_SZ where NULL terminator is used to separate strings. + * If not provided, default value 0 is used. + * @return {string} Converted JavaScript string. + */ +function fromNullTerminatedWString(buffer, start) { + if (start === undefined) { + start = 0; + } + + let foundNullTerminator = false; + let end = start; + while (end < buffer.length - 2) { + // In UTF-16 encoding, a NULL terminator is 2 bytes of zeros + if (buffer[end] === 0 && buffer[end + 1] === 0) { + foundNullTerminator = true; + break; + } + end += 2; + } + if (!foundNullTerminator) { + end = buffer.length; + } + + return fromWString(buffer, start, end); +} + +export { + toWString, + toNullTerminatedWString, + fromWString, + fromNullTerminatedWString, +};