diff --git a/CHANGELOG.md b/CHANGELOG.md index 124ff1d..b8c6d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Backtrace Node Release Notes +## Version 1.1.1 + +- fixed an issue with node machine id (https://github.com/backtrace-labs/backtrace-node/issues/34) + ## Version 1.1.0 - improved deduplication: the relative library path is used instead of absolute path for purposes of deduplication. diff --git a/package-lock.json b/package-lock.json index b2efb88..7b48633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "backtrace-node", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -642,6 +642,14 @@ "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", "dev": true }, + "native-reg": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/native-reg/-/native-reg-0.3.5.tgz", + "integrity": "sha512-lwaSAbq02DZ2aFK/ZvHzDpaHQSo7fh0GjIol+juMe6iwmeBO+ZMXFSPLfpOksOhcxgEBhgCU1bVm1G0u9lkdpA==", + "requires": { + "node-gyp-build": "^4" + } + }, "nock": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/nock/-/nock-13.1.0.tgz", @@ -654,10 +662,10 @@ "propagate": "^2.0.0" } }, - "node-machine-id": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", - "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" + "node-gyp-build": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" }, "normalize-path": { "version": "3.0.0", diff --git a/package.json b/package.json index 898792f..8a0ef3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "backtrace-node", - "version": "1.1.0", + "version": "1.1.1", "description": "Backtrace error reporting tool", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -33,7 +33,7 @@ "axios": "^0.21.1", "form-data": "^2.3.3", "json-stringify-safe": "^5.0.1", - "node-machine-id": "^1.1.10", + "native-reg": "^0.3.5", "source-scan": "~1.0.1", "tslib": "^1.10.0" }, diff --git a/source/helpers/machineId.ts b/source/helpers/machineId.ts new file mode 100644 index 0000000..4045d64 --- /dev/null +++ b/source/helpers/machineId.ts @@ -0,0 +1,138 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Aleksandr Komlev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* Based on source: https://github.com/Nokel81/node-machine-id/commit/ee2d03efca9e9ccb363e850093a2e6654137275c */ + +import { execSync } from 'child_process'; +import { createHash, pseudoRandomBytes } from 'crypto'; +import * as reg from 'native-reg'; +import { hostname } from 'os'; + +type SupportedPlatforms = 'darwin' | 'linux' | 'freebsd' | 'win32'; +const supportedPlatforms = ['darwin', 'linux', 'freebsd', 'win32']; + +export function getPlatform() { + const platform: SupportedPlatforms = process.platform as SupportedPlatforms; + if (supportedPlatforms.indexOf(platform) === -1) { + return null; + } + return platform; +} + +const platform: SupportedPlatforms | null = getPlatform(); + +const guid: { [index: string]: string } = { + darwin: 'ioreg -rd1 -c IOPlatformExpertDevice', + linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :', + freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid', +}; + +function hash(str: string): string { + return createHash('sha256').update(str).digest('hex'); +} + +function expose(result: string): string | null { + switch (platform) { + case 'darwin': + return result + .split('IOPlatformUUID')[1] + .split('\n')[0] + .replace(/\=|\s+|\"/gi, '') + .toLowerCase(); + case 'win32': + return result + .toString() + .replace(/\r+|\n+|\s+/gi, '') + .toLowerCase(); + case 'linux': + return result + .toString() + .replace(/\r+|\n+|\s+/gi, '') + .toLowerCase(); + case 'freebsd': + return result + .toString() + .replace(/\r+|\n+|\s+/gi, '') + .toLowerCase(); + default: + return null; + } +} + +function windowsMachineId(): string | null { + const regVal = reg.getValue(reg.HKEY.LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Cryptography', 'MachineGuid'); + if (regVal) { + return expose(regVal.toString()); + } else { + return null; + } +} + +function nonWindowsMachineId(): string | null { + try { + if (platform !== null && guid[platform]) { + return expose(execSync(guid[platform]).toString()); + } else { + return null; + } + } catch (_e) { + return null; + } +} + +export function generateUuid(name: string = hostname()): string { + const uuidSize = 16; + const bytes = name + ? Buffer.concat([Buffer.from(name, 'utf8'), Buffer.alloc(uuidSize)], uuidSize) + : pseudoRandomBytes(uuidSize); + return ( + bytes.slice(0, 4).toString('hex') + + '-' + + bytes.slice(4, 6).toString('hex') + + '-' + + bytes.slice(6, 8).toString('hex') + + '-' + + bytes.slice(8, 10).toString('hex') + + '-' + + bytes.slice(10, 16).toString('hex') + ); +} + +/** + * This function gets the OS native UUID/GUID synchronously, hashed by default. + * @param {boolean} [original=false] If true return original value of machine id, otherwise return hashed value (sha - 256) + */ +export function machineIdSync(original: boolean = false): string { + let id: string | null = null; + if (platform === 'win32') { + id = windowsMachineId(); + } else if (platform !== null) { + id = nonWindowsMachineId(); + } + if (id === null) { + id = generateUuid(); + } + + return original ? id : hash(id); +} diff --git a/source/model/backtraceReport.ts b/source/model/backtraceReport.ts index bf145ac..de20f67 100644 --- a/source/model/backtraceReport.ts +++ b/source/model/backtraceReport.ts @@ -1,6 +1,6 @@ import { pseudoRandomBytes } from 'crypto'; -import { machineIdSync } from 'node-machine-id'; import * as os from 'os'; +import { machineIdSync } from '../helpers/machineId'; import { readModule } from '../helpers/moduleResolver'; import { readMemoryInformation, readProcessStatus } from '../helpers/processHelper'; import { IBacktraceData } from './backtraceData'; @@ -42,7 +42,7 @@ export class BacktraceReport { // Backtrace-ndoe name public readonly agent = 'backtrace-node'; // Backtrace-node version - public readonly agentVersion = '1.1.0'; + public readonly agentVersion = '1.1.1'; // main thread name public readonly mainThread = 'main'; diff --git a/test/machineIdTest.ts b/test/machineIdTest.ts new file mode 100644 index 0000000..74f664c --- /dev/null +++ b/test/machineIdTest.ts @@ -0,0 +1,92 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Aleksandr Komlev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* Based on source: https://github.com/Nokel81/node-machine-id/commit/ee2d03efca9e9ccb363e850093a2e6654137275c */ + +import { assert } from 'chai'; +import { generateUuid, getPlatform, machineIdSync } from '../source/helpers/machineId'; + +let platform = getPlatform(), + originalPattern = { + darwin: /^[0-9,A-z]{8}-[0-9,A-z]{4}-[0-9,A-z]{4}-[0-9,A-z]{4}-[0-9,A-z]{12}$/, + win32: /^[0-9,A-z]{8}-[0-9,A-z]{4}-[0-9,A-z]{4}-[0-9,A-z]{4}-[0-9,A-z]{12}$/, + linux: /^[0-9,A-z]{32}$/, + freebsd: /^[0-9,A-z]{8}-[0-9,A-z]{4}-[0-9,A-z]{4}-[0-9,A-z]{4}-[0-9,A-z]{12}$/, + }, + hashPattern = /^[0-9,A-z]{64}$/; + +// these tests need to be run on each of the platforms above to test this in a comprehensive way +describe('Machine ID tests', () => { + describe('Sync call (original=true): machineIdSync(true)', function () { + it('should return original unique id', () => { + if (platform === null) { + throw 'null platform exception'; + } + assert.match(machineIdSync(true), originalPattern[platform]); + }); + }); + + describe('Sync call: machineIdSync()', function () { + it('should return unique sha256-hash', () => { + assert.match(machineIdSync(), hashPattern); + }); + }); + + describe('Uuid generation tests - based on the host name', () => { + it('should generate correctly uuid based on the host name that has less than 16 bytes', () => { + const testOsName = 'foo'; + const expectedUuid = `${Buffer.from(testOsName, 'utf8').toString('hex')}00-0000-0000-0000-000000000000`; + + const generatedUuid = generateUuid(testOsName); + + assert.equal(generatedUuid, expectedUuid); + }); + + it('should generate correctly uuid based on the host name that has more than 16 bytes', () => { + const testOsName = 'abcdabcdabcdabcdabcd123124nuabgyiagbygvba'; + const expectedUuid = '61626364-6162-6364-6162-636461626364'; + + const generatedUuid = generateUuid(testOsName); + + assert.equal(generatedUuid, expectedUuid); + }); + + it('should generate correctly uuid based on the host name that is undefined', () => { + const testOsName = undefined; + + const generatedUuid = generateUuid(testOsName); + + assert.isDefined(generatedUuid); + }); + + it('should generate the same uuid over different user sessions', () => { + const testOsName = 'abcd'; + + const firstSessionUuid = generateUuid(testOsName); + const secondSessionUuid = generateUuid(testOsName); + + assert.equal(firstSessionUuid, secondSessionUuid); + }); + }); +});