diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..4957ab1fe7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,48 @@ + + +# Summary + +In this pull request, the following changes are made: + +- Foobar was changed to FooFoo, because ... + + + +- Relates to #issue-number +- Resolves #issue-number + +
+Please follow this checklist to avoid unnecessary back and forth (click to expand) + +- [ ] 🧠 I used LLMs/AI in this contribution and reviewed all generated content. + I understand that I am responsible for and able to explain every line of code I submit. +- [ ] 🔍 Any UI changes adhere to visual style of this project. +- [ ] 🛠️ I have self-reviewed and self-tested my code to ensure it works as expected. +- [ ] 🤖 I added or updated automated tests where appropriate. +- [ ] 📄 Documentation updates are included (if applicable). +- [ ] 🧰 Dependency updates are listed and explained. +- [ ] ⚠️ CI passes and is green. + +
+ +## Screenshots for Visual Changes + + + +- **UI Modifications**: Highlight any changes made to the user interface. +- **Before & After**: Include screenshots or comparisons (if applicable). + +| Event | Before | After | +| ------------------ | --------------------- | -------------------- | +| `UP` | ![Before](image-link) | ![After](image-link) | +| `DOWN` | ![Before](image-link) | ![After](image-link) | +| Certificate-expiry | ![Before](image-link) | ![After](image-link) | +| Testing | ![Before](image-link) | ![After](image-link) | diff --git a/.github/workflows/docker-debian.yml b/.github/workflows/docker-debian.yml index 5b3ffc1302..9d08587093 100644 --- a/.github/workflows/docker-debian.yml +++ b/.github/workflows/docker-debian.yml @@ -121,7 +121,7 @@ jobs: file: ./docker/Dockerfile-debian tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 build-args: | DISABLE_MINIFY=yes DISABLE_TRANSLATE=yes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index acf164091f..69188b6f3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,6 @@ name: Release on: + workflow_dispatch: push: branches: - master @@ -12,14 +13,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Release - uses: justincy/github-action-npm-release@2.0.2 + uses: SerenModz21/github-action-npm-release@master id: release - with: - token: ${{ secrets.MY_TOKEN }} - name: Print release output if: ${{ steps.release.outputs.released == 'true' }} run: echo Release ID ${{ steps.release.outputs.release_id }} + - name: Trigger Docker builds + if: ${{ steps.release.outputs.released == 'true' }} + run: | + gh workflow run docker-alpine.yml -r ${{ steps.release.outputs.release_tag }} + gh workflow run docker-debian.yml -r ${{ steps.release.outputs.release_tag }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/agents/MeshCmd.exe b/agents/MeshCmd.exe index 00b704aaa5..759680fa97 100644 Binary files a/agents/MeshCmd.exe and b/agents/MeshCmd.exe differ diff --git a/agents/MeshCmd64.exe b/agents/MeshCmd64.exe index e4af314fcb..f74a9c5e02 100644 Binary files a/agents/MeshCmd64.exe and b/agents/MeshCmd64.exe differ diff --git a/agents/MeshCmdARM64.exe b/agents/MeshCmdARM64.exe index 83f1079a9a..fc711e695b 100644 Binary files a/agents/MeshCmdARM64.exe and b/agents/MeshCmdARM64.exe differ diff --git a/agents/MeshService.exe b/agents/MeshService.exe index e3aec7ab76..196e78accd 100644 Binary files a/agents/MeshService.exe and b/agents/MeshService.exe differ diff --git a/agents/MeshService64.exe b/agents/MeshService64.exe index 540fed64a7..da4c0719c2 100644 Binary files a/agents/MeshService64.exe and b/agents/MeshService64.exe differ diff --git a/agents/MeshServiceARM64.exe b/agents/MeshServiceARM64.exe index 9bceadf53a..f18a2bc4ea 100644 Binary files a/agents/MeshServiceARM64.exe and b/agents/MeshServiceARM64.exe differ diff --git a/agents/agents-feb2026/MeshCmd.exe b/agents/agents-feb2026/MeshCmd.exe deleted file mode 100644 index 759680fa97..0000000000 Binary files a/agents/agents-feb2026/MeshCmd.exe and /dev/null differ diff --git a/agents/agents-feb2026/MeshCmd64.exe b/agents/agents-feb2026/MeshCmd64.exe deleted file mode 100644 index f74a9c5e02..0000000000 Binary files a/agents/agents-feb2026/MeshCmd64.exe and /dev/null differ diff --git a/agents/agents-feb2026/MeshCmdARM64.exe b/agents/agents-feb2026/MeshCmdARM64.exe deleted file mode 100644 index fc711e695b..0000000000 Binary files a/agents/agents-feb2026/MeshCmdARM64.exe and /dev/null differ diff --git a/agents/agents-feb2026/MeshService.exe b/agents/agents-feb2026/MeshService.exe deleted file mode 100644 index 196e78accd..0000000000 Binary files a/agents/agents-feb2026/MeshService.exe and /dev/null differ diff --git a/agents/agents-feb2026/MeshService64.exe b/agents/agents-feb2026/MeshService64.exe deleted file mode 100644 index da4c0719c2..0000000000 Binary files a/agents/agents-feb2026/MeshService64.exe and /dev/null differ diff --git a/agents/agents-feb2026/MeshServiceARM64.exe b/agents/agents-feb2026/MeshServiceARM64.exe deleted file mode 100644 index f18a2bc4ea..0000000000 Binary files a/agents/agents-feb2026/MeshServiceARM64.exe and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_arm b/agents/agents-feb2026/meshagent_arm deleted file mode 100644 index 49745923e7..0000000000 Binary files a/agents/agents-feb2026/meshagent_arm and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_arm-linaro b/agents/agents-feb2026/meshagent_arm-linaro deleted file mode 100644 index cfbc067936..0000000000 Binary files a/agents/agents-feb2026/meshagent_arm-linaro and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_mips b/agents/agents-feb2026/meshagent_mips deleted file mode 100644 index a9da4347db..0000000000 Binary files a/agents/agents-feb2026/meshagent_mips and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_pogo b/agents/agents-feb2026/meshagent_pogo deleted file mode 100644 index 22a3da37c2..0000000000 Binary files a/agents/agents-feb2026/meshagent_pogo and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_poky b/agents/agents-feb2026/meshagent_poky deleted file mode 100644 index ef0ba511f5..0000000000 Binary files a/agents/agents-feb2026/meshagent_poky and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_poky64 b/agents/agents-feb2026/meshagent_poky64 deleted file mode 100644 index ef848974b1..0000000000 Binary files a/agents/agents-feb2026/meshagent_poky64 and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_x86 b/agents/agents-feb2026/meshagent_x86 deleted file mode 100644 index d812ba8d26..0000000000 Binary files a/agents/agents-feb2026/meshagent_x86 and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_x86-64 b/agents/agents-feb2026/meshagent_x86-64 deleted file mode 100644 index 41c1ddcf26..0000000000 Binary files a/agents/agents-feb2026/meshagent_x86-64 and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_x86-64_nokvm b/agents/agents-feb2026/meshagent_x86-64_nokvm deleted file mode 100644 index d3e223f5f6..0000000000 Binary files a/agents/agents-feb2026/meshagent_x86-64_nokvm and /dev/null differ diff --git a/agents/agents-feb2026/meshagent_x86_nokvm b/agents/agents-feb2026/meshagent_x86_nokvm deleted file mode 100644 index 19f6050c04..0000000000 Binary files a/agents/agents-feb2026/meshagent_x86_nokvm and /dev/null differ diff --git a/agents/hashagents.json b/agents/hashagents.json index b598fbeaf6..e59ec8d772 100644 --- a/agents/hashagents.json +++ b/agents/hashagents.json @@ -1,146 +1,146 @@ { "3": { "filename": "MeshService.exe", - "hash": "A28899C93AE7273C49433952149AD34ADB5A017A150A27243B69402D94BF4AAC32AAE6774C40EC8DE1B4CB33A82DC1C7", - "size": 3811840, - "mtime": "2025-03-07T17:05:07Z" + "hash": "7ABE621C6E9F8E1B11C842E052D890C942F285FEBB1A31897FD30EBCCD1F2A12873B3A6E14689FDC7E646352B903D0CA", + "size": 3816960, + "mtime": "2026-03-01T10:48:33Z" }, "4": { "filename": "MeshService64.exe", - "hash": "168572E0999089DE09B751255C908A4C2B80056D6DFAFFDB8B76F4A81847C8A4E162D6C303C14A837BBE8E6802AC6992", - "size": 3439616, - "mtime": "2025-03-07T17:05:07Z" + "hash": "319D3AD514173BE85838BF7BA5995E11671D8F8E6DF8E51B74C8304C3CAF70E754E69DB74B1BAEC1A6430ECE213AB293", + "size": 3444736, + "mtime": "2026-03-01T10:48:33Z" }, "5": { "filename": "meshagent_x86", - "hash": "EB1F41A192A43469823399E9880EF7435259A12E2687CF488B3CB17D86BA8C29ED20DD863C86D0D5CA1D40634DEB4134", - "size": 3641436, - "mtime": "2022-11-22T18:15:29Z" + "hash": "DC0BE75515B02E16978EC7EDD824C00462CEFD18F78209E79F221E760AEE366BB64D113077F768C59F66592F325ED7EB", + "size": 3649628, + "mtime": "2026-03-01T10:48:33Z" }, "6": { "filename": "meshagent_x86-64", - "hash": "434BF98C5D4F394CB5275C73FF96CB3283DA1300577784F51AF2273DE3158074E78A351C48A9CDEA3E7DC6DECD106128", - "size": 3749328, - "mtime": "2022-11-22T18:15:29Z" + "hash": "1BF04D9CED5C4BE006D1ED714DA04048D26E8F3FFDD02F39B69A73B6D73DA98682DDA7401EF33F04C621549AFF6C3A28", + "size": 3757520, + "mtime": "2026-06-02T10:53:33Z" }, "7": { "filename": "meshagent_mips", - "hash": "97F057410D979E5D290EC253B1340CDB4C1B55AFB2F766AC1CFACA368355E5D7CB86E51F44B34798F0FD1C691089859F", - "size": 4555960, - "mtime": "2022-11-22T18:15:29Z" + "hash": "482C484755FE7A00F9E5FAD1E83597C2AD99E87C5E1FB4C9E492C7896912EDB64419A6C153382E1C047EAF3AE3864221", + "size": 4564212, + "mtime": "2026-03-01T10:48:33Z" }, "9": { "filename": "meshagent_arm", - "hash": "963802249E86D8D0B08D62D688E67704F519CB1D6EA03A755CF3A277D30A1579E92E863F5E31B40CBBCF8C492D3B93E8", - "size": 3156256, - "mtime": "2022-11-22T18:15:29Z" + "hash": "8F9FB5DD78E2E888CB2C7106F59787DDCE7BCDB576B7BC2D7B79EF11F48BC143E606C9E6D43742160E20D74A30A661D2", + "size": 3164464, + "mtime": "2026-03-01T10:48:33Z" }, "13": { "filename": "meshagent_pogo", - "hash": "D07AD09AEBA3528785D1BE63D89785C1D2078EDA4F9E1ADE8B695A094DA4F3DB63BB16895E4BDEE733F54EF8C2F74265", - "size": 3164464, - "mtime": "2022-11-22T18:15:29Z" + "hash": "8B1640947C32AE4C0DDE4DD19B7704E6B85C3E17EB08EB12CAA11FD66EAEB1B1A2B720E5984FC84A35A43CF9F05FEA0D", + "size": 3172672, + "mtime": "2026-03-01T10:48:33Z" }, "15": { "filename": "meshagent_poky", - "hash": "515F9A5FFE2C229E24DB61770EEC1265F0A3AFC09C4321F1CA8D6C1A4C421FD223BDE85D94BC5B9E349EC7132CD9A7E5", - "size": 3804792, - "mtime": "2022-11-22T18:15:29Z" + "hash": "7F52448C051B4786DCA30E1CFF389377B2CDC6A5750D4BC7A697A342D2A03783DD6FEF156888D001632413F3F2041EEC", + "size": 3808888, + "mtime": "2026-03-01T10:48:33Z" }, "16": { "filename": "meshagent_osx-x86-64", - "hash": "F7A3EBEC3D855EBFB2C72271C17196C7692EB2685DCBA70B56B63C80D6CF0DAA7DF00657BB4A12F4C0D92281B1BB47FE", - "size": 4670736, - "mtime": "2022-08-29T20:54:17Z" + "hash": "90F804675A8CBF209CCE803F02F98F1C4202291FB8EBE302D003538EA958B4EB1A8607F79EF5EE19D2886194A2039118", + "size": 4243736, + "mtime": "2026-06-05T12:08:24Z" }, "18": { "filename": "meshagent_poky64", - "hash": "F889214564DBA1625DF17A7C3E14458D55350399112BAD429F8525B885EF2098EC527C3A83C3EBEF9BFB90A74AF4CD87", - "size": 3503608, - "mtime": "2022-11-22T18:15:29Z" + "hash": "447CCF992AE04BCB2D7F140540C2D8C148745B637851F3E2652A675701A8D724F33BB0A41ACD4839A8E1681A5AE01A72", + "size": 3511800, + "mtime": "2026-03-01T10:48:33Z" }, "19": { "filename": "meshagent_x86_nokvm", - "hash": "2558D5FB2B92AF81EE914F83BB0EC528C9D10BFAB67F9F7C27BFD6AB8516B93DDCF9F824BF0F307F47254C811D707434", - "size": 3360704, - "mtime": "2022-11-22T18:15:29Z" + "hash": "A7FA5A203669C051F15EB07B050EC8C2F2938FE45B932F6692E6352DB2519421237C984D0AA2B4B31E77633A98095429", + "size": 3368896, + "mtime": "2026-03-01T10:48:33Z" }, "20": { "filename": "meshagent_x86-64_nokvm", - "hash": "A91280CAB549B1F133408F283E850F35B529218C0C317007D60A170E6113DE262521B3DA0D1ECEAC0E945B3ED46D23F7", - "size": 3454384, - "mtime": "2022-11-22T18:15:29Z" + "hash": "16C1B8D248E6D49D0DD503B39355C35AC311E516E410B7E56D104E1023DF3B9BE31F39160828034F99A453910B849D3D", + "size": 3458480, + "mtime": "2026-03-01T10:48:33Z" }, "24": { "filename": "meshagent_arm-linaro", - "hash": "47E5DDAD71536DBB7752C665A4FE3BDD98D2AB888DB7536FB58E2EC7F0560C750AFE2D2D7F35A00457677661312380E6", - "size": 2215252, - "mtime": "2022-11-22T18:15:29Z" + "hash": "8E6E25B093038C3D4DA66D3AF19E7593649A1EB7CC3B4768C2ACEA1818B94750F0B9AE9799ECC3563273BAA58C4FDECF", + "size": 2223444, + "mtime": "2026-03-01T10:48:33Z" }, "25": { "filename": "meshagent_armhf", "hash": "65F6FE3B530FDBC3C1E7B2577B081FEF5742265CCCFDE27AB015996D3767F153CF3B4B022932C156754E09CDA2EE4874", "size": 3191132, - "mtime": "2022-11-22T18:15:29Z" + "mtime": "2026-03-01T10:48:34Z" }, "27": { "filename": "meshagent_armhf2", "hash": "0AE840520D3B677B9767EA097F3AA5A1E24212529E688200F43935DB1541AB9FB441EC2C7BA8002D45299B04695FD037", "size": 2837724, - "mtime": "2022-03-24T21:15:48Z" + "mtime": "2026-03-01T10:48:34Z" }, "28": { "filename": "meshagent_mips24kc", "hash": "3AD0A21E1FDD44E1869BFD3E3698D67713445C7A21BEE37AF41E810E6B6F1CE8FE369140AE7BAD31689A5F759CF7BCC6", "size": 4193384, - "mtime": "2022-11-22T18:15:29Z" + "mtime": "2026-03-01T10:48:34Z" }, "29": { "filename": "meshagent_osx-arm-64", - "hash": "CFE022146F2ED61E68F907E57E3704CC7F409D7F2B4D87E64ED6D83C53F41777BCDDCCF42DF8B8BB25CCEC9A93D799C7", - "size": 3945576, - "mtime": "2022-08-29T20:54:17Z" + "hash": "6E3FCFED24B6C28671E39445FDB8CD219040939DA1488CBD6C167AE1EB7E2E37382F6E34C8A22961F3E1BC8EF4171673", + "size": 3670136, + "mtime": "2026-06-05T12:08:20Z" }, "30": { "filename": "meshagent_freebsd_x86-64", "hash": "5C2CDDA2E7AB5068D990FBC725D8D5E3EA2724A0E001C226C0C7BB9F3A46492880BF260B5DD9E733F87EB68BA7494BD6", "size": 4673560, - "mtime": "2022-08-29T20:54:17Z" + "mtime": "2026-03-01T10:48:34Z" }, "32": { "filename": "meshagent_aarch64", "hash": "56AD2FAFBC15BAC635C526C6F106DD0213C0B222265685F00CDD56AD8C3DFE7DE6C4D8F52EA9CA183D50BC8D8C198477", "size": 3256688, - "mtime": "2022-11-22T18:15:29Z" + "mtime": "2026-03-01T10:48:33Z" }, "40": { "filename": "meshagent_mipsel24kc", "hash": "E6A3CDBFFD233BE1B24E81197F41DB24EFA1708B5BD18B2D7DFEBFD89FAF6A994B5D4A3048D67BBBAB14A77AD7088AE6", "size": 4189672, - "mtime": "2022-11-22T18:15:29Z" + "mtime": "2026-03-01T10:48:34Z" }, "41": { "filename": "meshagent_aarch64-cortex-a53", "hash": "CEFD8AAB52CE01939324E999E1EB541A262E0D4F1C3591F0A8355EAD5D2C87B43B3FB05A1DCD04012CDD4B559589CBAB", "size": 3084616, - "mtime": "2022-11-22T18:15:29Z" + "mtime": "2026-03-01T10:48:33Z" }, "43": { "filename": "MeshServiceARM64.exe", - "hash": "5D49143B18A7AF6B09378D57781B9A0910EDD394A17CC0E9D5518DF34E1442E306E0AF855073009885C4D0D45C431D61", - "size": 3634688, - "mtime": "2025-07-11T21:20:17Z" + "hash": "08A3372068B5F636C89B909D0DAC1D4FAFAC7FF8BBE55A78B27593DAE1606CEEF69BFA5078ECE112915272B96F8C46BB", + "size": 3637760, + "mtime": "2026-03-01T10:48:33Z" }, "45": { "filename": "meshagent_riscv64", "hash": "CFB8C78CB128B5F8FE367DE40ACD1D34BB9E261B8B37AF867F0B50130F847831B8C0DB4C9DAFC75F5DD80AFECCE74D30", "size": 3597928, - "mtime": "2025-03-18T18:11:04Z" + "mtime": "2026-03-01T10:48:34Z" }, "10005": { "filename": "meshagent_osx-universal-64", - "hash": "D320CA61D59FD8D76CF681CFE78A94CE37C47DBCAA8B29DF483F42C000EA9B655B5E5909A2AD6699D45D2D7691FF4964", - "size": 8647784, - "mtime": "2022-08-29T20:54:17Z" + "hash": "BE19AFEC2114D3CF348BB4C6B112C6634DB2A808A1973D3C5BF31725A752CC44EB359AB3BB740F4A15E392764E28D519", + "size": 7929976, + "mtime": "2026-06-05T12:08:22Z" } } diff --git a/agents/macosinstallerbackground.png b/agents/macosinstallerbackground.png new file mode 100644 index 0000000000..5296de458a Binary files /dev/null and b/agents/macosinstallerbackground.png differ diff --git a/agents/meshagent_arm b/agents/meshagent_arm index 7c857e5f2a..49745923e7 100644 Binary files a/agents/meshagent_arm and b/agents/meshagent_arm differ diff --git a/agents/meshagent_arm-linaro b/agents/meshagent_arm-linaro index 61f2063193..cfbc067936 100644 Binary files a/agents/meshagent_arm-linaro and b/agents/meshagent_arm-linaro differ diff --git a/agents/meshagent_mips b/agents/meshagent_mips index 4230b2e2f7..a9da4347db 100644 Binary files a/agents/meshagent_mips and b/agents/meshagent_mips differ diff --git a/agents/meshagent_osx-arm-64 b/agents/meshagent_osx-arm-64 index ff59636f93..b85919bd79 100644 Binary files a/agents/meshagent_osx-arm-64 and b/agents/meshagent_osx-arm-64 differ diff --git a/agents/meshagent_osx-universal-64 b/agents/meshagent_osx-universal-64 index e345004f97..899be4d79a 100644 Binary files a/agents/meshagent_osx-universal-64 and b/agents/meshagent_osx-universal-64 differ diff --git a/agents/meshagent_osx-x86-64 b/agents/meshagent_osx-x86-64 index 6000680993..e346b7bd3d 100644 Binary files a/agents/meshagent_osx-x86-64 and b/agents/meshagent_osx-x86-64 differ diff --git a/agents/meshagent_pogo b/agents/meshagent_pogo index 9ffbbff2c4..22a3da37c2 100644 Binary files a/agents/meshagent_pogo and b/agents/meshagent_pogo differ diff --git a/agents/meshagent_poky b/agents/meshagent_poky index ecbb530e6b..ef0ba511f5 100644 Binary files a/agents/meshagent_poky and b/agents/meshagent_poky differ diff --git a/agents/meshagent_poky64 b/agents/meshagent_poky64 index 0b260438c2..ef848974b1 100644 Binary files a/agents/meshagent_poky64 and b/agents/meshagent_poky64 differ diff --git a/agents/meshagent_x86 b/agents/meshagent_x86 index 33b5d9db11..d812ba8d26 100644 Binary files a/agents/meshagent_x86 and b/agents/meshagent_x86 differ diff --git a/agents/meshagent_x86-64 b/agents/meshagent_x86-64 index 0c3e00debd..41c1ddcf26 100644 Binary files a/agents/meshagent_x86-64 and b/agents/meshagent_x86-64 differ diff --git a/agents/meshagent_x86-64_nokvm b/agents/meshagent_x86-64_nokvm index 3f865a6116..d3e223f5f6 100644 Binary files a/agents/meshagent_x86-64_nokvm and b/agents/meshagent_x86-64_nokvm differ diff --git a/agents/meshagent_x86_nokvm b/agents/meshagent_x86_nokvm index 6d924a05b9..19f6050c04 100644 Binary files a/agents/meshagent_x86_nokvm and b/agents/meshagent_x86_nokvm differ diff --git a/agents/meshcmd.js b/agents/meshcmd.js index 347084c25a..5aca562263 100644 --- a/agents/meshcmd.js +++ b/agents/meshcmd.js @@ -305,16 +305,16 @@ function run(argv) { console.log(' --pass [password] The Intel AMT login password.'); console.log(' --agent [uuid] The unique identifier of the watchdog agent.'); } else if (action == 'amtpower') { - console.log('AmtPower will get current pwoer state or send a reboot command to a remote Intel AMT device. Example usage:\r\n\r\n meshcmd amtpower --reset --host 1.2.3.4 --user admin --pass mypassword --tls'); + console.log('AmtPower will get current power state or send a reboot command to a remote Intel AMT device. Example usage:\r\n\r\n meshcmd amtpower --reset --host 1.2.3.4 --user admin --pass mypassword --tls'); console.log('\r\nRequired arguments:\r\n'); console.log(' --host [hostname] The IP address or DNS name of Intel AMT.'); console.log(' --pass [password] The Intel AMT login password.'); console.log('\r\nOptional arguments:\r\n'); console.log(' --reset, --poweron, --poweroff, --powercycle, --sleep, --hibernate'); - console.log(' --user [username] The Intel AMT login username, admin is default.'); - console.log(' --tls Specifies that TLS must be used.'); - console.log(' --bootdevice [pxe|hdd|cd|ider-floppy|ider-cdrom] Specifies the boot device to use after reset, poweron or powercycle.'); - console.log(' --bootindex [number] Specifies the index of boot device to use.'); + console.log(' --user [username] The Intel AMT login username, admin is default.'); + console.log(' --tls Specifies that TLS must be used.'); + console.log(' --bootdevice [pxe|hdd|cd|bios|ider-floppy|ider-cdrom] Specifies the boot device to use after reset, poweron or powercycle.'); + console.log(' --bootindex [number] Specifies the index of boot device to use.'); } else if (action == 'amtnetwork') { console.log('AmtNetwork is used to get/set Intel AMT network interface configuration. Example usage:\r\n\r\n meshcmd amtnetwork --host 1.2.3.4 --user admin --pass mypassword --dhcp'); console.log('\r\nRequired arguments:\r\n'); @@ -873,7 +873,7 @@ function run(argv) { if (args.reset) { settings.poweraction = 10; } //if (settings.poweraction == 0) { console.log('No power action, specify --poweron, --sleep, --powercycle, --poweroff, --hibernate, --reset.'); exit(1); return; } // Accepted option for boot device are: pxe, hdd, cd - var bootdevices = ['pxe','hdd','cd']; + var bootdevices = ['pxe','hdd','cd','bios']; var ider_bootdevices = ['ider-floppy', 'ider-cdrom'] if (args.bootdevice) { if (bootdevices.indexOf(args.bootdevice.toLowerCase())>=0) { @@ -887,7 +887,7 @@ function run(argv) { settings.ider_bootindex = ider_bootdevices.indexOf(args.bootdevice.toLowerCase()); settings.ider_boot = true; } else { - console.log('Supported boot devices are pxe, hdd, cd, ider-floppy, ider-cdrom'); exit(1); return; + console.log('Supported boot devices are pxe, hdd, cd, bios, ider-floppy, ider-cdrom'); exit(1); return; } } // boot index for cd and hdd @@ -3165,8 +3165,8 @@ function powerActionResponse1(stack, name, response, status) { r['ConfigurationDataReset'] = false; r['BIOSPause'] = false; r['EnforceSecureBoot'] = false; - r['BIOSSetup'] = false; - if (settings.bootdevice && settings.bootdevice!='pxe') { + r['BIOSSetup'] = (settings.bootdevice === 'bios'); + if (settings.bootdevice && settings.bootdevice!='pxe' && settings.bootdevice!='bios') { r['BootMediaIndex'] = settings.bootindex; } else { r['BootMediaIndex'] = 0; diff --git a/agents/meshcore.js b/agents/meshcore.js index 319618b15f..0c4adf98c7 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -44,6 +44,7 @@ var MESHRIGHT_LIMITEVENTS = 8192; var MESHRIGHT_CHATNOTIFY = 16384; var MESHRIGHT_UNINSTALL = 32768; var MESHRIGHT_NODESKTOP = 65536; +var MESHRIGHT_ADMIN = 0xFFFFFFFF; var pendingSetClip = false; // This is a temporary hack to prevent multiple setclips at the same time to stop the agent from crashing. @@ -88,7 +89,7 @@ function getDomainInfo() { switch (process.platform) { case 'win32': try { - ret = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_ComputerSystem', ['Name', 'Domain', 'PartOfDomain'])[0]; + ret = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT * FROM Win32_ComputerSystem', ['Name', 'Domain', 'PartOfDomain'])[0]; } catch (x) { } @@ -309,9 +310,11 @@ function getCoreTranslation() { setDefaultCoreTranslation(ret, 'terminalConsent', '{0} requesting remote terminal access. Grant access?'); setDefaultCoreTranslation(ret, 'desktopConsent', '{0} requesting remote desktop access. Grant access?'); setDefaultCoreTranslation(ret, 'fileConsent', '{0} requesting remote file Access. Grant access?'); + setDefaultCoreTranslation(ret, 'registryConsent', '{0} requesting remote registry access. Grant access?'); setDefaultCoreTranslation(ret, 'terminalNotify', '{0} started a remote terminal session.'); setDefaultCoreTranslation(ret, 'desktopNotify', '{0} started a remote desktop session.'); setDefaultCoreTranslation(ret, 'fileNotify', '{0} started a remote file session.'); + setDefaultCoreTranslation(ret, 'registryNotify', '{0} started a remote registry session.'); setDefaultCoreTranslation(ret, 'privacyBar', 'Sharing desktop with: {0}'); return (ret); @@ -535,6 +538,18 @@ function addAmtEvent(msg) { if (obj.showamtevent) { require('MeshAgent').SendCommand({ action: 'msg', type: 'console', value: e }); } } function zeroPad(num, size) { var s = '000000000' + num; return s.substr(s.length - size); } +function trimResults(val) { + var i, x; + for (i = 0; i < val.length; ++i) { + for (x in val[i]) { + if (x.startsWith('_')) { + delete val[i][x]; + } else { + if (val[i][x] == null || val[i][x] == 0) { delete val[i][x]; } + } + } + } +} // Create Secure IPC for Diagnostic Agent Communications @@ -864,7 +879,211 @@ var wifiScanner = null; var networkMonitor = null; var nextTunnelIndex = 1; var apftunnel = null; -var tunnelUserCount = { terminal: {}, files: {}, tcp: {}, udp: {}, msg: {} }; // List of userid->count sessions for terminal, files and TCP/UDP routing +var tunnelUserCount = { terminal: {}, files: {}, registry: {}, tcp: {}, udp: {}, msg: {} }; // List of userid->count sessions for terminal, files, registry and TCP/UDP routing + +function getRegistryRoots() { + return ['HKEY_LOCAL_MACHINE', 'HKEY_CURRENT_USER', 'HKEY_USERS', 'HKEY_CLASSES_ROOT', 'HKEY_CURRENT_CONFIG']; +} + +function getRegistryHiveEnum(hiveName) { + var registry = require('win-registry'); + switch (hiveName) { + case 'HKEY_LOCAL_MACHINE': return registry.HKEY.LocalMachine; + case 'HKEY_CURRENT_USER': return registry.HKEY.CurrentUser; + case 'HKEY_USERS': return registry.HKEY.Users; + case 'HKEY_CLASSES_ROOT': return registry.HKEY.ClassesRoot; + case 'HKEY_CURRENT_CONFIG': return registry.HKEY.CurrentConfig; + default: return null; + } +} + +function guessRegistryValueType(value) { + if (value == null) { return 'REG_NONE'; } + if (typeof value == 'number') { return ((Math.floor(value) === value) && (value >= 0) && (value <= 0xFFFFFFFF)) ? 'REG_DWORD' : 'REG_QWORD'; } + if (typeof value == 'string') { return 'REG_SZ'; } + if (Array.isArray(value)) { return 'REG_MULTI_SZ'; } + if (Buffer.isBuffer(value)) { return 'REG_BINARY'; } + return 'REG_UNKNOWN'; +} + +function getRegistryValueType(hiveName, path, valueName, fallbackValue) { + try { + var fullPath = hiveName + (((path != null) && (path !== '')) ? ('\\' + path) : ''); + var args = ['query', fullPath]; + if ((valueName == null) || (valueName === '')) { args.push('/ve'); } else { args.push('/v', valueName); } + var output = runRegistryCommand(args, true); + if (typeof output == 'string') { + var lines = output.split(/\r?\n/); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if ((line == '') || (line.indexOf('HKEY_') == 0)) { continue; } + var parts = line.split(/\s{2,}/); + if ((parts.length >= 2) && (parts[1].indexOf('REG_') == 0)) { return parts[1]; } + } + } + } catch (ex) { } + return guessRegistryValueType(fallbackValue); +} + +function listRegistryKey(hiveName, path) { + var registry = require('win-registry'); + var hive = getRegistryHiveEnum(hiveName); + if (hive == null) { throw ('Unknown registry hive: ' + hiveName); } + + var result = registry.QueryKey(hive, path); + if (result == null) { return { hive: hiveName, path: path, subkeys: [], values: [] }; } + + var response = { hive: hiveName, path: path, subkeys: (result.subkeys || []), values: [] }; + if (result.values != null) { + for (var i = 0; i < result.values.length; i++) { + var valueName = result.values[i], valueData = null, valueText = '', valueType = 'REG_UNKNOWN'; + try { valueData = registry.QueryKey(hive, path, valueName); } catch (ex) { valueData = null; } + valueType = getRegistryValueType(hiveName, path, valueName, valueData); + if (valueData == null) { valueText = ''; } + else if (typeof valueData == 'object') { + try { valueText = JSON.stringify(valueData); } catch (ex) { valueText = String(valueData); } + } else { + valueText = String(valueData); + } + response.values.push({ name: ((valueName === '') ? '(Default)' : valueName), rawname: valueName, type: valueType, value: valueText }); + } + } + return response; +} + +function getRegistryFullPath(hiveName, path) { + return hiveName + (((path != null) && (path !== '')) ? ('\\' + path) : ''); +} + +function getRegistryExecutableCandidates() { + return ['C:\\Windows\\Sysnative\\reg.exe', 'C:\\Windows\\System32\\reg.exe', 'C:\\WINNT\\System32\\reg.exe']; +} + +function runRegistryCommand(args, returnOutput) { + var fs = require('fs'); + var child = null, childProcess = require('child_process'), lastExecError = null, executable = null, candidates = getRegistryExecutableCandidates(), execArgs = null; + for (var i = 0; i < candidates.length; i++) { + executable = candidates[i]; + if ((executable.indexOf('\\') >= 0) && (fs.existsSync(executable) == false)) { continue; } + try { + execArgs = ['reg.exe'].concat(args); + child = childProcess.execFile(executable, execArgs); + break; + } catch (ex) { + lastExecError = ex; + child = null; + } + } + if (child == null) { + if (lastExecError != null) { throw ('child_process.execFile(): Could not exec [' + candidates.join(', ') + '] (' + lastExecError + ')'); } + throw ('child_process.execFile(): Could not exec [' + candidates.join(', ') + ']'); + } + child.stdout.str = ''; + child.stderr.str = ''; + child.stdout.on('data', function (chunk) { this.str += chunk.toString(); }); + child.stderr.on('data', function (chunk) { this.str += chunk.toString(); }); + child.waitExit(); + if ((child.exitCode != null) && (child.exitCode !== 0)) { + if ((child.stderr.str != null) && (child.stderr.str.trim() != '')) { throw (child.stderr.str.trim()); } + if ((child.stdout.str != null) && (child.stdout.str.trim() != '')) { throw (child.stdout.str.trim()); } + throw ('Registry command failed with exit code ' + child.exitCode + ' using ' + executable + '.'); + } + if ((child.stderr.str != null) && (child.stderr.str.trim() != '')) { throw (child.stderr.str.trim()); } + if (returnOutput === true) { return child.stdout.str || ''; } + return child.stdout.str || ''; +} + +function createRegistrySubKey(hiveName, path, keyName) { + if ((keyName == null) || (keyName === '')) { throw ('Registry key name is required.'); } + if (keyName.indexOf('\\') >= 0) { throw ('Registry key name cannot contain backslashes.'); } + runRegistryCommand(['add', getRegistryFullPath(hiveName, ((path != null) && (path !== '')) ? (path + '\\' + keyName) : keyName), '/f']); +} + +function deleteRegistryEntries(items) { + if ((items == null) || (items.length == 0)) { throw ('Nothing selected for deletion.'); } + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if ((item == null) || (item.hive == null)) { continue; } + if (item.kind == 'key') { + runRegistryCommand(['delete', getRegistryFullPath(item.hive, ((item.path != null) && (item.path !== '')) ? (item.path + '\\' + item.name) : item.name), '/f']); + } else if (item.kind == 'value') { + var args = ['delete', getRegistryFullPath(item.hive, item.path || '')]; + if ((item.name == null) || (item.name === '')) { args.push('/ve'); } else { args.push('/v', item.name); } + args.push('/f'); + runRegistryCommand(args); + } else { + throw ('Deleting registry hives is not allowed.'); + } + } +} + +function setRegistryValue(hiveName, path, valueName, valueType, valueData) { + if ((valueType == null) || (valueType === '')) { throw ('Registry value type is required.'); } + valueType = valueType.toUpperCase(); + if ((valueType == 'REG_DWORD') || (valueType == 'REG_QWORD')) { + if ((typeof valueData != 'string') || (/^(0x[0-9a-fA-F]+|[0-9]+)$/.test(valueData.trim()) == false)) { throw ('Only decimal or 0x-prefixed numeric data is supported for ' + valueType + '.'); } + valueData = valueData.trim(); + } else if (valueData == null) { + valueData = ''; + } else { + valueData = String(valueData); + } + var args = ['add', getRegistryFullPath(hiveName, path || '')]; + if ((valueName == null) || (valueName === '')) { args.push('/ve'); } else { args.push('/v', valueName); } + args.push('/t', valueType, '/d', valueData, '/f'); + runRegistryCommand(args); +} + +function renameRegistryEntry(item, newName) { + if ((item == null) || (item.hive == null)) { throw ('Nothing selected for rename.'); } + if ((newName == null) || (newName === '')) { throw ('A new registry name is required.'); } + if (newName.indexOf('\\') >= 0) { throw ('Registry names cannot contain backslashes.'); } + if (item.kind == 'key') { + var oldKeyPath = ((item.path != null) && (item.path !== '')) ? (item.path + '\\' + item.name) : item.name; + var newKeyPath = ((item.path != null) && (item.path !== '')) ? (item.path + '\\' + newName) : newName; + runRegistryCommand(['copy', getRegistryFullPath(item.hive, oldKeyPath), getRegistryFullPath(item.hive, newKeyPath), '/s', '/f']); + runRegistryCommand(['delete', getRegistryFullPath(item.hive, oldKeyPath), '/f']); + return; + } + if (item.kind == 'value') { + if ((item.name == null) || (item.name === '')) { throw ('The default registry value cannot be renamed in this increment.'); } + var hive = getRegistryHiveEnum(item.hive), valueData = null, valueType = 'REG_UNKNOWN'; + if (hive == null) { throw ('Unknown registry hive: ' + item.hive); } + try { valueData = require('win-registry').QueryKey(hive, item.path || '', item.name); } catch (ex) { valueData = null; } + valueType = getRegistryValueType(item.hive, item.path || '', item.name, valueData); + setRegistryValue(item.hive, item.path || '', newName, valueType, valueData); + deleteRegistryEntries([{ kind: 'value', hive: item.hive, path: item.path || '', name: item.name }]); + return; + } + throw ('Registry hives cannot be renamed.'); +} + +// reg.exe writes .reg files as UTF-16LE with a BOM, the agent has no utf16 decoder. +// Decode in chunks, a per-char string concat would be O(n^2) on a large export (HKLM\SOFTWARE is tens of MB). +function utf16leToString(buf) { + var start = ((buf.length > 1) && (buf[0] == 0xFF) && (buf[1] == 0xFE)) ? 2 : 0; + var parts = [], chunk = []; + for (var i = start; (i + 1) < buf.length; i += 2) { + chunk.push(buf[i] + (buf[i + 1] * 256)); + if (chunk.length == 8192) { parts.push(String.fromCharCode.apply(null, chunk)); chunk = []; } + } + if (chunk.length > 0) { parts.push(String.fromCharCode.apply(null, chunk)); } + return parts.join(''); +} + +function exportRegistryKey(hiveName, path) { + if ((path == null) || (path === '')) { throw ('Select a registry key to export.'); } + var fs = require('fs'); + // Write to the agent folder, not to a shared temp folder, the export can hold sensitive keys + var tmpFolder = (process.cwd() != '//') ? process.cwd() : ((process.env['ProgramData'] || 'C:\\ProgramData') + '\\MeshAgent\\'); + var tmpFile = tmpFolder + 'mesh-registry-export-' + Date.now() + '.reg'; + try { + runRegistryCommand(['export', getRegistryFullPath(hiveName, path), tmpFile, '/y']); + return utf16leToString(fs.readFileSync(tmpFile)); + } finally { + try { fs.unlinkSync(tmpFile); } catch (ex) { } + } +} // Add to the server event log function MeshServerLog(msg, state) { @@ -1342,6 +1561,7 @@ function handleServerCommand(data) { tunnel.consentAutoAcceptIfTerminalLocked = (tunnel.soptions && (tunnel.soptions.consentAutoAcceptIfTerminalLocked === true)); tunnel.consentAutoAcceptIfFileLocked = (tunnel.soptions && (tunnel.soptions.consentAutoAcceptIfFileLocked === true)); tunnel.oldStyle = (tunnel.soptions && tunnel.soptions.oldStyle) ? tunnel.soptions.oldStyle : false; + tunnel.terminalUserVariable = (tunnel.soptions && tunnel.soptions.terminalUserVariable) ? tunnel.soptions.terminalUserVariable : false; tunnel.tcpaddr = data.tcpaddr; tunnel.tcpport = data.tcpport; tunnel.udpaddr = data.udpaddr; @@ -1590,14 +1810,14 @@ function handleServerCommand(data) { if (require('MeshAgent').isService) { require('clipboard').dispatchRead().then(function (str) { if (str) { - MeshServerLogEx(21, [str.length], "Getting clipboard content, " + str.length + " byte(s)", data); + if (data.tag != 3) { MeshServerLogEx(21, [str.length], "Getting clipboard content, " + str.length + " byte(s)", data); } mesh.SendCommand({ action: 'msg', type: 'getclip', sessionid: data.sessionid, data: str, tag: data.tag }); } }); } else { require('clipboard').read().then(function (str) { if (str) { - MeshServerLogEx(21, [str.length], "Getting clipboard content, " + str.length + " byte(s)", data); + if (data.tag != 3) { MeshServerLogEx(21, [str.length], "Getting clipboard content, " + str.length + " byte(s)", data); } mesh.SendCommand({ action: 'msg', type: 'getclip', sessionid: data.sessionid, data: str, tag: data.tag }); } }); @@ -1695,6 +1915,192 @@ function handleServerCommand(data) { } break; } + case 'software': { + var sendSoftwareResponse = function(responseData) { + mesh.SendCommand({ + action: 'software', + value: (typeof responseData === 'string') ? responseData : JSON.stringify(responseData), + sessionid: data.sessionid + }); + }; + if (data.type == 'installedapps') { + if (process.platform == 'win32') { + try { + if (require('win-info').installedApps) { + require('win-info').installedApps().then(sendSoftwareResponse).catch(function(e) { sendSoftwareResponse({ error: e.toString() }); }); + } else { sendSoftwareResponse({ error: "Not supported" }); } + } catch (e) { sendSoftwareResponse({ error: e.toString() }); } + } else if (process.platform == 'linux') { + try { + if (require('linux-info').packages) { + require('linux-info').packages().then(sendSoftwareResponse).catch(function(e) { sendSoftwareResponse({ error: e.toString() }); }); + } else { sendSoftwareResponse({ error: "Not supported" }); } + } catch (e) { sendSoftwareResponse({ error: e.toString() }); } + } else if (process.platform == 'darwin') { + try { + if (require('mac-info').apps) { + require('mac-info').apps().then(sendSoftwareResponse).catch(function(e) { sendSoftwareResponse({ error: e.toString() }); }); + } else { sendSoftwareResponse({ error: "Not supported" }); } + } catch (e) { sendSoftwareResponse({ error: e.toString() }); } + } else { + sendSoftwareResponse({ success: false, error: "Not supported" }); + } + } else if (data.type == 'installedstoreapps') { + if (process.platform != 'win32') { + sendSoftwareResponse({ success: false, error: "Installed Store Apps is only supported on Windows devices" }); + return; + } + try { + if (require('win-info').installedStoreApps) { + require('win-info').installedStoreApps().then(sendSoftwareResponse).catch(function(e) { sendSoftwareResponse({ error: e.toString() }); }); + } + } catch (e) { sendSoftwareResponse({ error: e.toString() }); } + } else if (data.type == 'uninstallapp' && (typeof data.value == 'string' && data.value != '')) { + if (process.platform != 'win32') { + sendSoftwareResponse({ success: false, error: "Uninstall is only supported on Windows devices" }); + return; + } + var base64Cmd = data.value.trim(); + var uninstallCmd = ''; + try { + var b = Buffer.from(base64Cmd, 'base64'); + var decoded = b.toString(); + var lc = decoded ? decoded.toLowerCase() : ''; + if (decoded && decoded.length > 0 && (lc.indexOf('msiexec') >= 0 || lc.indexOf('.exe') >= 0)) { uninstallCmd = decoded; } else { uninstallCmd = base64Cmd; } + } catch (e) { uninstallCmd = base64Cmd; } + if (!uninstallCmd || uninstallCmd.trim() === '' || uninstallCmd.trim() === '\\') { + sendSoftwareResponse({ success: false, error: 'No valid uninstall command available' }); + } else { + var logDir = (process.env['ProgramData'] || 'C:\\ProgramData') + '\\MeshAgent'; + try { if (!require('fs').existsSync(logDir)) { require('fs').mkdirSync(logDir); } } catch (e) { } + var logFile = logDir + '\\MeshAgent_Uninstall.log'; + var child_process = require('child_process'); + var cmdPath = process.env['windir'] + '\\system32\\cmd.exe'; + var writeLog = function(message, callback) { + try { + var timestamp = new Date().toISOString(); + var logLine = timestamp + ' | ' + message; + logLine = logLine.replace(/"/g, '""').replace(/&/g, '^&').replace(//g, '^>').replace(/\|/g, '^|'); + var logChild = child_process.execFile(cmdPath, ['cmd', '/c', 'echo ' + logLine + ' >> "' + logFile + '"'], { timeout: 5000 }); + logChild.on('exit', function() { if (callback) callback(); }); + } catch (e) { if (callback) callback(); } + }; + try { + writeLog('UNINSTALL START - Command: ' + uninstallCmd.replace(/\\/g, '/')); + var originalCmd = uninstallCmd; + if (uninstallCmd.toLowerCase().indexOf('msiexec') >= 0) { + uninstallCmd = uninstallCmd.replace(/\/I\s*(\{[^}]+\})/gi, '/X $1'); // change /I to /X for uninstall, fixes 7zip example + uninstallCmd = uninstallCmd.replace(/\/q[nbrf]?/gi, ''); + if (uninstallCmd.indexOf('/QN') < 0) { uninstallCmd = uninstallCmd + ' /QN /norestart'; } + } else { + if (uninstallCmd.toLowerCase().indexOf('/s') < 0) { uninstallCmd = uninstallCmd + ' /S /silent /SILENT /VERYSILENT /quiet /norestart'; } + } + uninstallCmd = uninstallCmd.replace(/\s+/g, ' ').trim(); + var child = child_process.execFile(cmdPath, ['cmd', '/c', uninstallCmd], { timeout: 300000 }); + child.stdout.str = ''; child.stderr.str = ''; + child._cmd = uninstallCmd; + child.stdout.on('data', function(c) { this.str += c.toString(); }); + child.stderr.on('data', function(c) { this.str += c.toString(); }); + child.on('exit', function(code) { + var success = (code === 0 || code === null || code === 3010); + var status = success ? 'SUCCESS' : 'FAILED'; + writeLog('UNINSTALL ' + status + ' - ExitCode: ' + code); + var result = { success: success, exitCode: code, command: child._cmd }; + if (child.stdout.str) result.stdout = child.stdout.str.trim().substring(0, 500); + if (child.stderr.str) result.stderr = child.stderr.str.trim().substring(0, 500); + sendSoftwareResponse(result); + }); + sendSoftwareResponse({ status: 'Silent uninstall started', command: uninstallCmd }); + } catch (ex) { + writeLog('UNINSTALL ERROR - ' + ex.toString()); + sendSoftwareResponse({ error: ex.toString() }); + } + } + } else if (data.type == 'uninstallstoreapp' && (typeof data.value === 'string' && data.value != '')) { + if (process.platform != 'win32') { + sendSoftwareResponse({ success: false, error: "Uninstall is only supported on Windows devices" }); + return; + } + var rawName = data.value.trim(); + var packageName = rawName.replace(/"/g, "").replace(/'/g, ""); + var logDir = (process.env['ProgramData'] || 'C:\\ProgramData') + '\\MeshAgent'; + try { if (!require('fs').existsSync(logDir)) { require('fs').mkdirSync(logDir); } } catch (e) { } + var logFile = logDir + '\\MeshAgent_StoreUninstall.log'; + try { + var psPath = (process.env['SystemRoot'] || 'C:\\Windows') + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; + var child_process = require('child_process'); + var completionMarker = '###DONE###' + Date.now(); + var child = child_process.execFile(psPath, ['powershell', '-NoProfile', '-NoLogo', '-ExecutionPolicy', 'Bypass', '-Command', '-'], {}); + child.stdout.str = ''; child.stderr.str = ''; + child._completed = false; + child.stdout.on('data', function(c) { + this.str += c.toString(); + if (!child._completed && this.str.indexOf(completionMarker) >= 0) { + child._completed = true; + var output = this.str.split(completionMarker)[0].trim(); + sendSoftwareResponse({ status: 'Finished', output: output }); + } + }); + child.stderr.on('data', function(c) { this.str += c.toString(); }); + var script = [ + "$LogFile = '" + logFile + "'", + "$TargetName = '" + packageName + "'", + "function Write-Log { param([string]$Msg); $Line = \"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Msg\"; Add-Content -Path $LogFile -Value $Line -ErrorAction SilentlyContinue; Write-Output $Msg }", + "", + "Write-Log \"STORE UNINSTALL START: $TargetName\"", + "Write-Log \"Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)\"", + "", + "$foundCount = 0", + "$pkgs = Get-AppxPackage -AllUsers -Name \"*$TargetName*\"", + "if ($pkgs) {", + " if ($pkgs -isnot [array]) { $pkgs = @($pkgs) }", + " Write-Log \"Found (AllUsers): $($pkgs.Count) packages\"", + " foreach ($p in $pkgs) {", + " Write-Log \"Removing: $($p.PackageFullName)\"", + " try {", + " Remove-AppxPackage -Package $p.PackageFullName -AllUsers -ErrorAction Stop", + " Write-Log \"SUCCESS: Removed with -AllUsers\"", + " $foundCount++", + " } catch {", + " Write-Log \"WARN: AllUsers failed, trying current user only. Error: $($_.Exception.Message)\"", + " try {", + " Remove-AppxPackage -Package $p.PackageFullName -ErrorAction Stop", + " Write-Log \"SUCCESS: Removed from CurrentUser\"", + " $foundCount++", + " } catch {", + " Write-Log \"ERROR: Failed to remove $($p.PackageFullName). Error: $($_.Exception.Message)\"", + " }", + " }", + " }", + "} else { Write-Log \"Found (AllUsers): 0 packages\" }", + "", + "$prov = Get-AppxProvisionedPackage -Online | Where-Object { $_.PackageName -like \"*$TargetName*\" }", + "if ($prov) {", + " if ($prov -isnot [array]) { $prov = @($prov) }", + " Write-Log \"Found provisioned: $($prov.Count) packages\"", + " foreach ($pr in $prov) {", + " Write-Log \"Deprovisioning: $($pr.DisplayName)\"", + " try {", + " Remove-AppxProvisionedPackage -Online -PackageName $pr.PackageName -ErrorAction Stop | Out-Null", + " Write-Log \"SUCCESS: Deprovisioned\"", + " } catch {", + " Write-Log \"ERROR: Failed to deprovision. Error: $($_.Exception.Message)\"", + " }", + " }", + "} else { Write-Log \"Found provisioned: 0 packages\" }", + "", + "'" + completionMarker + "'", + "exit" + ].join("\r\n"); + child.stdin.write(script + "\r\n"); + setTimeout(function() { if (!child._completed) { child.kill(); sendSoftwareResponse({ error: 'Timeout' }); } }, 60000); + sendSoftwareResponse({ status: 'Store app removal started', package: packageName }); + } catch (ex) { + sendSoftwareResponse({ error: ex.toString() }); + } + } + break; + } case 'acmactivate': { if (amt != null) { MeshServerLogEx(23, null, "Attempting Intel AMT ACM mode activation", data); @@ -2126,6 +2532,12 @@ function getSystemInformation(func) { replaceSpacesWithUnderscoresRec(results); var hasher = require('SHA384Stream').create(); + var finalizeResults = function () + { + results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); + func(results); + }; + // On Windows platforms, get volume information - Needs more testing. if (process.platform == 'win32') { @@ -2133,23 +2545,19 @@ function getSystemInformation(func) { if (require('win-volumes').volumes_promise != null) { var p = require('win-volumes').volumes_promise(); - p.then(function (res) - { - results.hardware.windows.volumes = cleanGetBitLockerVolumeInfo(res); - results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); - func(results); + p.then(function (res) { + if (res && res.drives) { try { results.hardware.windows.volumes = cleanGetBitLockerVolumeInfo(res.drives); } catch (ex) { } } + finalizeResults(); }); } else { - results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); - func(results); + finalizeResults(); } } else { - results.hash = hasher.syncHash(JSON.stringify(results)).toString('hex'); - func(results); + finalizeResults(); } } catch (ex) { func(null, ex); } @@ -2716,7 +3124,15 @@ function terminal_promise_consent_resolved() var env = { HISTCONTROL: 'ignoreboth' }; if (process.env['LANG']) { env['LANG'] = process.env['LANG']; } if (process.env['PATH']) { env['PATH'] = process.env['PATH']; } - env['MESHCENTRAL_USER'] = (this.httprequest.userid ? this.httprequest.userid.split('/')[2] : (this.httprequest.guestuserid ? 'deviceshare:' + this.httprequest.guestuserid.split('/')[2] : 'unknown')); + if (typeof this.httprequest.terminalUserVariable == 'string' && this.httprequest.terminalUserVariable != '') { + if (this.httprequest.terminalUserVariable == 'realname') { + env['MESHCENTRAL_USER'] = (this.httprequest.realname ? this.httprequest.realname : 'unknown'); + } else if (this.httprequest.terminalUserVariable == 'identifier') { + env['MESHCENTRAL_USER'] = (this.httprequest.userid ? this.httprequest.userid : (this.httprequest.guestuserid ? 'deviceshare:' + this.httprequest.guestuserid : 'unknown')); + } else if (this.httprequest.terminalUserVariable == 'username') { + env['MESHCENTRAL_USER'] = (this.httprequest.username ? this.httprequest.username : 'unknown'); + } + } if (this.httprequest.xoptions) { if (this.httprequest.xoptions.rows) { env.LINES = ('' + this.httprequest.xoptions.rows); } @@ -3147,6 +3563,96 @@ function files_tunnel_endhandler() if (this._consentpromise && this._consentpromise.close) { this._consentpromise.close(); } } +function registry_consent_ok(ws){ + if (ws.httprequest.consent && (ws.httprequest.consent & 0x0080)) { + MeshServerLogEx(165, null, "Started remote registry with toast notification (" + ws.httprequest.remoteaddr + ")", ws.httprequest); + var notifyMessage = currentTranslation['registryNotify'].replace(/\{0\}/g, ws.httprequest.realname); + var notifyTitle = "MeshCentral"; + if (ws.httprequest.soptions != null) { + if (ws.httprequest.soptions.notifyTitle != null) { notifyTitle = ws.httprequest.soptions.notifyTitle; } + if (ws.httprequest.soptions.notifyMsgRegistry != null) { notifyMessage = ws.httprequest.soptions.notifyMsgRegistry.replace(/\{0\}/g, ws.httprequest.realname).replace(/\{1\}/g, ws.httprequest.username); } + } + try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { } + } else { + MeshServerLogEx(166, null, "Started remote registry without notification (" + ws.httprequest.remoteaddr + ")", ws.httprequest); + } + ws.resume(); +} + +function registry_consent_ask(ws){ + ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: "Waiting for user to grant access...", msgid: 1 })); + var consentMessage = currentTranslation['registryConsent'].replace(/\{0\}/g, ws.httprequest.realname).replace(/\{1\}/g, ws.httprequest.username); + var consentTitle = 'MeshCentral'; + if (ws.httprequest.soptions != null) { + if (ws.httprequest.soptions.consentTitle != null) { consentTitle = ws.httprequest.soptions.consentTitle; } + if (ws.httprequest.soptions.consentMsgRegistry != null) { consentMessage = ws.httprequest.soptions.consentMsgRegistry.replace(/\{0\}/g, ws.httprequest.realname).replace(/\{1\}/g, ws.httprequest.username); } + } + var pr; + if (process.platform == 'win32') { + var enhanced = false; + if (ws.httprequest.oldStyle === false) { try { require('win-userconsent'); enhanced = true; } catch (ex) { } } + if (enhanced) { + var ipr = server_getUserImage(ws.httprequest.userid); + ipr.consentTitle = consentTitle; + ipr.consentMessage = consentMessage; + ipr.consentTimeout = ws.httprequest.consentTimeout; + ipr.consentAutoAccept = ws.httprequest.consentAutoAccept; + ipr.username = ws.httprequest.realname; + ipr.tsid = ws.tsid; + ipr.translations = { Allow: currentTranslation['allow'], Deny: currentTranslation['deny'], Auto: currentTranslation['autoAllowForFive'], Caption: consentMessage }; + pr = ipr.then(function (img) { + this.consent = require('win-userconsent').create(this.consentTitle, this.consentMessage, this.username, { b64Image: img.split(',').pop(), uid: this.tsid, timeout: this.consentTimeout * 1000, timeoutAutoAccept: this.consentAutoAccept, translations: this.translations, background: color_options.background, foreground: color_options.foreground }); + this.__childPromise.close = this.consent.close.bind(this.consent); + return (this.consent); + }); + } else { + pr = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout, null); + } + } else { + pr = require('message-box').create(consentTitle, consentMessage, ws.httprequest.consentTimeout, null); + } + pr.ws = ws; + ws.pause(); + ws._consentpromise = pr; + ws.prependOnceListener('end', files_tunnel_endhandler); + pr.then(registry_consentpromise_resolved, registry_consentpromise_rejected); +} + +function registry_consentpromise_resolved(always) +{ + if (always && process.platform == 'win32') { server_set_consentTimer(this.ws.httprequest.userid); } + this.ws._consentpromise = null; + MeshServerLogEx(163, null, "Starting remote registry after local user accepted (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + this.ws.write(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: null })); + if (this.ws.httprequest.consent && (this.ws.httprequest.consent & 0x0080)) { + var notifyMessage = currentTranslation['registryNotify'].replace(/\{0\}/g, this.ws.httprequest.realname); + var notifyTitle = "MeshCentral"; + if (this.ws.httprequest.soptions != null) { + if (this.ws.httprequest.soptions.notifyTitle != null) { notifyTitle = this.ws.httprequest.soptions.notifyTitle; } + if (this.ws.httprequest.soptions.notifyMsgRegistry != null) { notifyMessage = this.ws.httprequest.soptions.notifyMsgRegistry.replace(/\{0\}/g, this.ws.httprequest.realname).replace(/\{1\}/g, this.ws.httprequest.username); } + } + try { require('toaster').Toast(notifyTitle, notifyMessage); } catch (ex) { } + } + this.ws.resume(); + this.ws = null; +} + +function registry_consentpromise_rejected(e) +{ + if (this.ws) { + if (this.ws.httprequest) { + if ((this.ws.httprequest.oldStyle === true) && (this.ws.httprequest.consentAutoAccept === true) && (e.toString() != "7")) { + registry_consentpromise_resolved.call(this); + return; + } + MeshServerLogEx(164, null, "Failed to start remote registry after local user rejected (" + this.ws.httprequest.remoteaddr + ")", this.ws.httprequest); + } + this.ws._consentpromise = null; + this.ws.end(JSON.stringify({ ctrlChannel: '102938', type: 'console', msg: e.toString(), msgid: 2 })); + this.ws = null; + } +} + function onTunnelData(data) { //sendConsoleText('OnTunnelData, ' + data.length + ', ' + typeof data + ', ' + data); @@ -3176,7 +3682,7 @@ function onTunnelData(data) } else { // Handle tunnel data - if (this.httprequest.protocol == 0) { // 1 = Terminal (admin), 2 = Desktop, 5 = Files, 6 = PowerShell (admin), 7 = Plugin Data Exchange, 8 = Terminal (user), 9 = PowerShell (user), 10 = FileTransfer + if (this.httprequest.protocol == 0) { // 1 = Terminal (admin), 2 = Desktop, 4 = Registry, 5 = Files, 6 = PowerShell (admin), 7 = Plugin Data Exchange, 8 = Terminal (user), 9 = PowerShell (user), 10 = FileTransfer // Take a look at the protocol if ((data.length > 3) && (data[0] == '{')) { onTunnelControlData(data, this); return; } this.httprequest.protocol = parseInt(data); @@ -3446,6 +3952,46 @@ function onTunnelData(data) this.removeAllListeners('data'); this.on('data', onTunnelControlData); //this.write('MeshCore KVM Hello!1'); + } else if (this.httprequest.protocol == 4) { + // + // Remote Registry + // + + // Check user access rights for registry + if ((this.httprequest.rights & MESHRIGHT_REMOTECONTROL) == 0) { + // Disengage this tunnel, user does not have the rights to do this!! + this.httprequest.protocol = 999999; + this.httprequest.s.end(); + sendConsoleText("Error: No registry control rights."); + return; + } + + this.descriptorMetadata = "Remote Registry"; + + // Add the registry session to the count to update the server + if (this.httprequest.userid != null) { + var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest); + if (tunnelUserCount.registry[userid] == null) { tunnelUserCount.registry[userid] = 1; } else { tunnelUserCount.registry[userid]++; } + try { mesh.SendCommand({ action: 'sessions', type: 'registry', value: tunnelUserCount.registry }); } catch (ex) { } + broadcastSessionsToRegisteredApps(); + } + + this.end = function () + { + // Remove the registry session from the count to update the server + if (this.httprequest.userid != null) { + var userid = getUserIdAndGuestNameFromHttpRequest(this.httprequest); + if (tunnelUserCount.registry[userid] != null) { tunnelUserCount.registry[userid]--; if (tunnelUserCount.registry[userid] <= 0) { delete tunnelUserCount.registry[userid]; } } + try { mesh.SendCommand({ action: 'sessions', type: 'registry', value: tunnelUserCount.registry }); } catch (ex) { } + broadcastSessionsToRegisteredApps(); + } + }; + + if (this.httprequest.consent && (this.httprequest.consent & 0x0100)) { + registry_consent_ask(this); + } else { + registry_consent_ok(this); + } } else if (this.httprequest.protocol == 5) { // // Remote Files @@ -3555,6 +4101,103 @@ function onTunnelData(data) } else { this.httprequest.desktop.write(data); } + } else if (this.httprequest.protocol == 4) { + // Process registry commands + var cmd = null; + try { cmd = JSON.parse(data); } catch (ex) { }; + if (cmd == null) { return; } + if ((cmd.ctrlChannel == '102938') || ((cmd.type == 'offer') && (cmd.sdp != null))) { onTunnelControlData(cmd, this); return; } // If this is control data, handle it now. + if (cmd.action == undefined) { return; } + + switch (cmd.action) { + case 'listroots': { + var response = { action: 'listroots', reqid: cmd.reqid, roots: [] }; + if (process.platform != 'win32') { + response.error = 'Registry is currently supported on Windows agents only.'; + } else { + response.roots = getRegistryRoots(); + } + this.write(JSON.stringify(response)); + break; + } + case 'list': { + var response = { action: 'list', reqid: cmd.reqid, hive: cmd.hive, path: cmd.path, subkeys: [], values: [] }; + if (process.platform != 'win32') { + response.error = 'Registry is currently supported on Windows agents only.'; + } else { + try { + var listResponse = listRegistryKey(cmd.hive, cmd.path || ''); + response.hive = listResponse.hive; + response.path = listResponse.path; + response.subkeys = listResponse.subkeys; + response.values = listResponse.values; + } catch (ex) { + response.error = (ex && ex.message) ? ex.message : String(ex); + } + } + this.write(JSON.stringify(response)); + break; + } + case 'createkey': { + var response = { action: 'createkey', reqid: cmd.reqid, hive: cmd.hive, path: cmd.path, name: cmd.name }; + if (process.platform != 'win32') { + response.error = 'Registry is currently supported on Windows agents only.'; + } else { + try { createRegistrySubKey(cmd.hive, cmd.path || '', cmd.name); response.success = true; } + catch (ex) { response.error = (ex && ex.message) ? ex.message : String(ex); } + } + this.write(JSON.stringify(response)); + break; + } + case 'delete': { + var response = { action: 'delete', reqid: cmd.reqid }; + if (process.platform != 'win32') { + response.error = 'Registry is currently supported on Windows agents only.'; + } else { + try { deleteRegistryEntries(cmd.items || []); response.success = true; } + catch (ex) { response.error = (ex && ex.message) ? ex.message : String(ex); } + } + this.write(JSON.stringify(response)); + break; + } + case 'setvalue': { + var response = { action: 'setvalue', reqid: cmd.reqid, hive: cmd.hive, path: cmd.path, name: cmd.name, type: cmd.type }; + if (process.platform != 'win32') { + response.error = 'Registry is currently supported on Windows agents only.'; + } else { + try { setRegistryValue(cmd.hive, cmd.path || '', cmd.name, cmd.type, cmd.value); response.success = true; } + catch (ex) { response.error = (ex && ex.message) ? ex.message : String(ex); } + } + this.write(JSON.stringify(response)); + break; + } + case 'rename': { + var response = { action: 'rename', reqid: cmd.reqid }; + if (process.platform != 'win32') { + response.error = 'Registry is currently supported on Windows agents only.'; + } else { + try { renameRegistryEntry(cmd.item, cmd.newName); response.success = true; } + catch (ex) { response.error = (ex && ex.message) ? ex.message : String(ex); } + } + this.write(JSON.stringify(response)); + break; + } + case 'export': { + var response = { action: 'export', reqid: cmd.reqid, hive: cmd.hive, path: cmd.path }; + if (process.platform != 'win32') { + response.error = 'Registry is currently supported on Windows agents only.'; + } else { + try { response.success = true; response.content = exportRegistryKey(cmd.hive, cmd.path || ''); } + catch (ex) { response.error = (ex && ex.message) ? ex.message : String(ex); } + } + this.write(JSON.stringify(response)); + break; + } + default: { + this.write(JSON.stringify({ action: cmd.action, reqid: cmd.reqid, error: 'Unknown registry action.' })); + break; + } + } } else if (this.httprequest.protocol == 5) { // Process files commands var cmd = null; @@ -3974,6 +4617,11 @@ function onTunnelControlData(data, ws) { case 'close': { // We received the close on the websocket //sendConsoleText('Tunnel #' + ws.tunnel.index + ' WebSocket control close'); + // Attempt to send EOF (Ctrl-D) multiple times to exit nested shells (screen, su, etc.) cleanly, + // This allows the shell to write its history before the process is killed + if (process.platform != 'win32' && ws.httprequest && ws.httprequest.process && ws.httprequest.process.stdin) { + try { ws.httprequest.process.stdin.write('\x04\x04\x04'); } catch (ex) { } + } try { ws.close(); } catch (ex) { } break; } @@ -4222,17 +4870,18 @@ function processConsoleCommand(cmd, args, rights, sessionid) { switch (cmd) { case 'help': { // Displays available commands var fin = '', f = '', availcommands = 'domain,translations,agentupdate,errorlog,msh,timerinfo,coreinfo,coreinfoupdate,coredump,service,fdsnapshot,fdcount,startupoptions,'; - availcommands += 'alert,agentsize,versions,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,wslist,plugin,wsconnect,wssend,wsclose,notify,'; - availcommands += 'ls,ps,kill,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,openurl,getscript,getclip,setclip,log,cpuinfo,sysinfo'; - availcommands += 'apf,scanwifi,wallpaper,agentmsg,task,uninstallagent,display,openfile'; + availcommands += 'alert,agentsize,versions,help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbdelete,dbcompact,eval,parseuri,httpget,wslist,plugin,wsconnect,wssend,wsclose,notify,'; + availcommands += 'ls,ps,kill,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,openurl,getscript,getclip,setclip,log,cpuinfo,sysinfo,'; + availcommands += 'apf,scanwifi,wallpaper,agentmsg,task,uninstallagent,display,openfile,installedapps'; if (require('os').dns != null) { availcommands += ',dnsinfo'; } try { require('linux-dhcp'); availcommands += ',dhcp'; } catch (ex) { } if (process.platform == 'win32') { - availcommands += ',bitlocker,cs,wpfhwacceleration,uac,volumes,rdpport,domaininfo'; + availcommands += ',bitlocker,cs,wpfhwacceleration,uac,volumes,rdpport,domaininfo,printers,wmi'; if (bcdOK()) { availcommands += ',safemode'; } if (require('notifybar-desktop').DefaultPinned != null) { availcommands += ',privacybar'; } try { require('win-utils'); availcommands += ',taskbar'; } catch (ex) { } - try { require('win-info'); availcommands += ',installedapps,qfe,defender,av,installedstoreapps'; } catch (ex) { } + try { require('win-info'); availcommands += ',qfe,defender,av,installedstoreapps'; } catch (ex) { } + try { require('win-updates'); availcommands += ',winupdates,winupdatesinstalled,winupdateshistory,winupdatesdedup'; } catch (ex) { } try { require('win-deskutils'); availcommands += ',mousetrails,idletime,deskbackground'; } catch (ex) { } } if (amt != null) { availcommands += ',amt,amtconfig,amtevents'; } @@ -4313,6 +4962,79 @@ function processConsoleCommand(cmd, args, rights, sessionid) { } } break; + case 'printers': + if (process.platform != 'win32') { + response = 'Unknown command "printers", type "help" for list of available commands.'; + } else { + var wmi = require('win-wmi-fixed'); + var printers = wmi.query('ROOT\\CIMV2', 'SELECT * FROM Win32_Printer'); + trimResults(printers); + var tcpPorts = wmi.query('ROOT\\CIMV2', 'SELECT Name, HostAddress, PortNumber FROM Win32_TCPIPPrinterPort'); + trimResults(tcpPorts); + var portMap = {}; + for (var j = 0; j < tcpPorts.length; ++j) { portMap[tcpPorts[j].Name] = tcpPorts[j].HostAddress + ':' + tcpPorts[j].PortNumber; } + // For vendor ports not covered by Win32_TCPIPPrinterPort, walk the registry under Print\Monitors + try { + var reg = require('win-registry'); + var HKLM = reg.HKEY.LocalMachine; + var monitorsKey = 'SYSTEM\\CurrentControlSet\\Control\\Print\\Monitors'; + var monitors = reg.QueryKey(HKLM, monitorsKey); + if (monitors && monitors.keys) { + for (var m = 0; m < monitors.keys.length; ++m) { + var portsKey = monitorsKey + '\\' + monitors.keys[m] + '\\Ports'; + try { + var portsNode = reg.QueryKey(HKLM, portsKey); + if (portsNode && portsNode.keys) { + for (var p = 0; p < portsNode.keys.length; ++p) { + var portName = portsNode.keys[p]; + if (portMap[portName]) continue; + var portKey = portsKey + '\\' + portName; + var ip = null; + try { ip = reg.QueryKey(HKLM, portKey, 'IPAddress'); } catch (e) {} + if (!ip) { try { ip = reg.QueryKey(HKLM, portKey, 'HostName'); } catch (e) {} } + if (ip) { portMap[portName] = ip; } + } + } + } catch (e) {} + } + } + } catch (e) {} + // For Epson and other vendor ports still missing, query ROOT\StandardCimv2\MSFT_PrinterPort + try { + var msftPorts = wmi.query('ROOT\\StandardCimv2', 'SELECT Name, Description FROM MSFT_PrinterPort'); + trimResults(msftPorts); + for (var j = 0; j < msftPorts.length; ++j) { + if (!portMap[msftPorts[j].Name] && msftPorts[j].Description) { + portMap[msftPorts[j].Name] = msftPorts[j].Description; + } + } + } catch (e) {} + var printJobs = wmi.query('ROOT\\CIMV2', 'SELECT Name FROM Win32_PrintJob'); + trimResults(printJobs); + var jobCount = {}; + for (var j = 0; j < printJobs.length; ++j) { + var jobPrinter = printJobs[j].Name.split(',')[0]; + jobCount[jobPrinter] = (jobCount[jobPrinter] || 0) + 1; + } + var printerStatusMap = { 1: 'Other', 2: 'Unknown', 3: 'Idle', 4: 'Printing', 5: 'Warmup', 6: 'Stopped', 7: 'Offline' }; + var errorStateMap = { 0: 'Unknown', 1: 'Other', 2: 'No Error', 3: 'Low Paper', 4: 'No Paper', 5: 'Low Toner', 6: 'No Toner', 7: 'Door Open', 8: 'Jammed', 9: 'Offline', 10: 'Service Requested', 11: 'Output Bin Full' }; + for (var i = 0; i < printers.length; ++i) { + var portDesc = portMap[printers[i].PortName]; + var jobs = jobCount[printers[i].Name] || 0; + var status = printerStatusMap[printers[i].PrinterStatus] || 'Unknown'; + var errors = []; + var err = parseInt(printers[i].DetectedErrorState) || 0; + if (err > 2) { errors.push(errorStateMap[err] || ('Error ' + err)); } + var line = printers[i].Name + + ' - ' + printers[i].PortName + + (portDesc ? ' (' + portDesc + ')' : '') + + ' [' + status + ']' + + (errors.length > 0 ? ' [' + errors.join(', ') + ']' : '') + + (jobs > 0 ? ' [' + jobs + ' job' + (jobs > 1 ? 's' : '') + ' queued]' : ''); + sendConsoleText(line, sessionid); + } + } + break; case 'privacybar': if (process.platform != 'win32' || require('notifybar-desktop').DefaultPinned == null) { response = 'Unknown command "privacybar", type "help" for list of available commands.'; @@ -4357,7 +5079,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) { } sendConsoleText('Querying Domain Controller... This can take up to 60 seconds. Please wait...', sessionid); - global._domainQuery = require('win-wmi').queryAsync('ROOT\\CIMV2', 'SELECT * FROM Win32_NTDomain'); + global._domainQuery = require('win-wmi-fixed').queryAsync('ROOT\\CIMV2', 'SELECT * FROM Win32_NTDomain'); global._domainQuery.session = sessionid; global._domainQuery.then(function (v) { var results = []; @@ -4387,6 +5109,41 @@ function processConsoleCommand(cmd, args, rights, sessionid) { }); break; } + case 'wmi': + if (process.platform != 'win32') { + response = 'Unknown command "wmi", type "help" for list of available commands.'; + break; + } + if (args['_'].length < 2 || args['_'].length > 3) { + response = 'Execute a WMI query.\r\nUsage: wmi namespace "query" [(n)sync][(u)npretty][(s)ilence][(i)ncludesystemproperties]\r\n' + + 'Default: async, pretty and shows progress\r\n' + + 'Note: Sync queries block the agent for the duration of the query\r\n' + + 'Example: wmi CIMV2 "SELECT Name,ProcessId FROM Win32_Process WHERE Name=\'meshagent.exe\'" in\r\n'; + break; + } + var opt = (args['_'][2]|| '').toLowerCase(); + var ns = args['_'][0].trim(); + if (!/^root\\\w/i.test(ns)) { ns = 'ROOT\\' + ns; } + var q = (args['_'][1]).trim(); + var indent = (opt.indexOf('u') === -1); // unpretty, so default true + var showProgress = (opt.indexOf('s') === -1); //s is now silence + var includeSysProp = (opt.indexOf('i') !== -1); + var async = opt.indexOf('n') === -1; + var wmi = require('win-wmi-fixed'); + var output = function (res) { sendConsoleText((res && res.length > 0) ? (JSON.stringify(res, null, (indent ? 2 : 0))) : 'No results', sessionid); }; + var error = function (e) { + e = e || {}; + sendConsoleText(((e.results && e.results.length) ? 'Partial result:\r\n' + JSON.stringify(e.results, null, (indent ? 2 : 0)) + '\r\nPartial resultcount: ' + e.results.length + '\r\n' : '') + + 'Error: ' + (e.message || JSON.stringify(e)), sessionid); }; + if (showProgress) { sendConsoleText('Performing ' + (async ? 'asynchronous ': '') + 'query ' + (async ? '': 'synchronously (be careful with large queries as it blocks the agent for the duration)'), sessionid); } + if (async) { + wmi.queryAsync(ns, q, null, includeSysProp, null, (showProgress ? sessionid : null)) + .then( output ) + .catch( error ); + } else { + try { output(wmi.query(ns, q, null, includeSysProp, null, (showProgress ? sessionid : null) )); } catch (e) { error(e); } + } + break; case 'translations': { response = JSON.stringify(coretranslations, null, 2); break; @@ -4396,10 +5153,37 @@ function processConsoleCommand(cmd, args, rights, sessionid) { break; case 'bitlocker': if (process.platform == 'win32') { - if (require('win-volumes').volumes_promise != null) { - var p = require('win-volumes').volumes_promise(); - p.then(function (res) { sendConsoleText(JSON.stringify(cleanGetBitLockerVolumeInfo(res), null, 1), this.session); }); + if (require('user-sessions').isRoot()) { + if (require('win-volumes').volumes_promise != null) { + var p = require('win-volumes').volumes_promise(); + p.sessionid = sessionid; + p.then(function (res) { + if (res && res.error) { sendConsoleText('Bitlocker error: ' + (res.error.message ? res.error.message : res.error), this.sessionid); } + var drives = (res && res.drives && (Object.keys(res.drives).length !== 0)) ? res.drives : null; // win-volumes returns 'drives' as at least {}, so also check on empty object + if (drives) { + var conversionStatuses = { 0: 'FullyDecrypted', 1: 'FullyEncrypted', 2: 'EncryptionInProgress', 3: 'DecryptionInProgress', 4: 'EncryptionPaused', 5: 'DecryptionPaused' }; + var encryptionMethods = { 0: 'None', 1: 'AES_128_WITH_DIFFUSER', 2: 'AES_256_WITH_DIFFUSER', 3: 'AES_128', 4: 'AES_256', 5: 'HARDWARE_ENCRYPTION', 6: 'XTS_AES_128', 7: 'XTS_AES_256' }; + var protectionStatuses = { 0: 'Off', 1: 'On', 2: 'Locked'}; + var driveType = { 0: "Unknown", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" }; + for (var i in drives) { + const v = drives[i]; + if (conversionStatuses[v.volumeStatus]) { v.volumeStatus = conversionStatuses[v.volumeStatus]; } else { v.volumeStatus = 'Unknown'; } + if (encryptionMethods[v.encryptionMethod]) { v.encryptionMethod = encryptionMethods[v.encryptionMethod]; } else { v.encryptionMethod = 'Unknown'; } + if (protectionStatuses[v.protectionStatus]) { v.protectionStatus = protectionStatuses[v.protectionStatus]; } else { v.protectionStatus = 'Unknown'; } + if (driveType[v.dType]) {v.dType = driveType[v.dType]; } else { v.dType = 'Unknown'; } + if (v.recoveryPassword && (rights != MESHRIGHT_ADMIN)) { v.recoveryPassword = '(Only admins)'; } + } + } + sendConsoleText(JSON.stringify((drives ? drives : 'No volume/bitlocker info'), null, 1), this.sessionid); + }); + } else { + sendConsoleText('BitLocker info not available.', sessionid); + } + } else { + sendConsoleText('Bitlocker info requires an elevated agent', sessionid); } + } else { + sendConsoleText('BitLocker is only supported on Windows.', sessionid); } break; case 'dhcp': // This command is only supported on Linux, this is because Linux does not give us the DNS suffix for each network adapter independently so we have to ask the DHCP server. @@ -5100,6 +5884,9 @@ function processConsoleCommand(cmd, args, rights, sessionid) { if (process.platform == 'win32') { // Windows Command: "wmic /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct get /FORMAT:CSV" response = JSON.stringify(require('win-info').av(), null, 1); + } else if (process.platform == 'linux') { + require('linux-info').av(function (r) { sendConsoleText(JSON.stringify(r, null, 1), sessionid); }); + response = null; } else { response = 'Not supported on the platform'; } @@ -5271,7 +6058,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) { if (args['_'].length < 1) { response = 'Proper usage: eval "JavaScript code"'; // Display correct command usage } else { - response = JSON.stringify(mesh.eval(args['_'][0])); // This can only be run by trusted administrator. + var evalResult = mesh.eval(args['_'][0]); try { response = JSON.stringify(evalResult); } catch (ex) { response = null; } // This can only be run by trusted administrator. } break; } @@ -5321,8 +6108,31 @@ function processConsoleCommand(cmd, args, rights, sessionid) { if (results == null) { sendConsoleText(err, this.sessionid); } else { - sendConsoleText(JSON.stringify(results, null, 1), this.sessionid); + // Send the full data to the server first mesh.SendCommand({ action: 'sysinfo', sessionid: this.sessionid, data: results }); + // Mask BitLocker recovery keys for non-admins in the console output only and optionally replace codes with strings + pretty = !(args['_'].length > 0 && args['_'][0] == 'raw'); + const encMethod = { 0: '', 1: "AES-128 with diffuser", 2: "AES-256 with diffuser", 3: 'AES-128', 4: 'AES-256', 5: "Hardware encryption", 6: 'XTS-AES-128', 7: 'XTS-AES-256' }; + const driveType = { 0: "Unknown", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" }; + const conversionStatus = { "-1": "Unknown", 1: "Fully Encrypted", 2: "Encryption In Progress", 3: "Decryption In Progress", 4: "Encryption Paused", 5: "Decryption Paused" }; + const protectionStatus = { 0: "Off", 1: "On", 2: "Locked"}; + var replacer = function (k, val) { + if ( k === 'recoveryPassword' && rights != MESHRIGHT_ADMIN) { return '(Only admins)'; } + else if (pretty && typeof val === 'number') { + switch (k) { + case 'encryptionMethod': + return (val >= 1 && val <= 7) ? encMethod[val] : val; + case 'protectionStatus': + return (val >= 0 && val <= 2) ? protectionStatus[val] : val; + case 'volumeStatus': + return (val >= -1 && val <= 5) ? conversionStatus[val] : val; + case 'dType': + return (val >= 1 && val <= 6) ? driveType[val]: val; + } + } + return val; + }; + sendConsoleText(JSON.stringify(results, replacer, 1), this.sessionid); } }); break; @@ -5381,15 +6191,16 @@ function processConsoleCommand(cmd, args, rights, sessionid) { break; } case 'dbkeys': { // Return all data store keys + if (db == null) { response = 'Database not accessible.'; break; } response = JSON.stringify(db.Keys); break; } case 'dbget': { // Return the data store value for a given key if (db == null) { response = 'Database not accessible.'; break; } if (args['_'].length != 1) { - response = 'Proper usage: dbget (key)'; // Display the value for a given database key + response = 'Proper usage: dbget (key)'; } else { - response = db.Get(args['_'][0]); + response = (db.Get(args['_'][0]) != null ? db.Get(args['_'][0]) : 'Key not found'); } break; } @@ -5399,14 +6210,26 @@ function processConsoleCommand(cmd, args, rights, sessionid) { response = 'Proper usage: dbset (key) (value)'; // Set a database key } else { var r = db.Put(args['_'][0], args['_'][1]); - response = 'Key set: ' + r; + response = r == 0 ? 'Key set' : 'Failed to set key'; + } + break; + } + case 'dbdelete': { // Delete a data store key + if (db == null) { response = 'Database not accessible.'; break; } + if (args['_'].length != 1) { + response = 'Proper usage: dbdelete (key)'; // Delete a database key + } else { + var existed = db.Get(args['_'][0]) != null; + db.Delete(args['_'][0]); // new agent with Delete fix + db.Delete('0/' + args['_'][0]); // old agent without Delete fix + response = existed ? 'Key deleted' : 'Key not found'; } break; } case 'dbcompact': { // Compact the data store if (db == null) { response = 'Database not accessible.'; break; } var r = db.Compact(); - response = 'Database compacted: ' + r; + response = r == 0 ? 'Database compacted' : 'Compact failed'; break; } case 'httpget': { @@ -5552,7 +6375,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) { var interfaces = require('os').networkInterfaces(); if (process.platform == 'win32') { try { - var ret = require('win-wmi').query('ROOT\\CIMV2', 'SELECT InterfaceIndex,NetConnectionID,Speed FROM Win32_NetworkAdapter', ['InterfaceIndex','NetConnectionID','Speed']); + var ret = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT InterfaceIndex,NetConnectionID,Speed FROM Win32_NetworkAdapter', ['InterfaceIndex','NetConnectionID','Speed']); if (ret[0]) { var speedMap = {}; for (var i = 0; i < ret.length; i++) speedMap[ret[i].InterfaceIndex] = ret[i].Speed; @@ -5776,17 +6599,393 @@ function processConsoleCommand(cmd, args, rights, sessionid) { break; } case 'installedapps': { - if(process.platform == 'win32'){ - require('win-info').installedApps().then(function (apps){ sendConsoleText(JSON.stringify(apps,null,1)); }); + if (process.platform == 'win32') { + try { + sendConsoleText('Fetching installed apps, please wait...', sessionid); + var iaPr = require('win-info').installedApps(); + iaPr.sessionid = sessionid; + iaPr.then(function (apps) { + if (apps.length === 0) { sendConsoleText('No installed apps found.', this.sessionid); return; } + var lines = ['Installed Apps (' + apps.length + '):']; + for (var i = 0; i < apps.length; i++) { + var a = apps[i]; + var line = ' [' + (i + 1) + '] ' + a.name; + if (a.version) line += ' (v' + a.version + ')'; + if (a.publisher) line += ' - ' + a.publisher; + if (a.date) line += ' [Installed: ' + a.date + ']'; + if (a.location) line += '\n Location: ' + a.location; + lines.push(line); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { + sendConsoleText('Module win-info error: ' + ex, sessionid); + } + } else if (process.platform == 'linux') { + try { + sendConsoleText('Fetching installed apps, please wait...', sessionid); + var iaLPr = require('linux-info').packages(); + iaLPr.sessionid = sessionid; + iaLPr.then(function (apps) { + if (apps.length === 0) { sendConsoleText('No installed apps found.', this.sessionid); return; } + var lines = ['Installed Apps (' + apps.length + '):']; + for (var i = 0; i < apps.length; i++) { + var a = apps[i]; + var line = ' [' + (i + 1) + '] ' + a.name; + if (a.version) line += ' (v' + a.version + ')'; + if (a.arch) line += ' [' + a.arch + ']'; + if (a.publisher) line += ' - ' + a.publisher; + if (a.date) line += ' [Installed: ' + a.date + ']'; + if (a.location) line += ' (' + a.location + ')'; + lines.push(line); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { + sendConsoleText('Module linux-info error: ' + ex, sessionid); + } + } else if (process.platform == 'darwin') { + try { + sendConsoleText('Fetching installed apps, please wait...', sessionid); + var iaMPr = require('mac-info').apps(); + iaMPr.sessionid = sessionid; + iaMPr.then(function (apps) { + if (apps.length === 0) { sendConsoleText('No installed apps found.', this.sessionid); return; } + var lines = ['Installed Apps (' + apps.length + '):']; + for (var i = 0; i < apps.length; i++) { + var a = apps[i]; + var line = ' [' + (i + 1) + '] ' + a.name; + if (a.version) line += ' (v' + a.version + ')'; + if (a.arch) line += ' [' + a.arch + ']'; + if (a.publisher) line += ' - ' + a.publisher; + if (a.date) line += ' [Installed: ' + a.date + ']'; + if (a.location) line += ' (' + a.location + ')'; + lines.push(line); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { + sendConsoleText('Module mac-info error: ' + ex, sessionid); + } + } else { + sendConsoleText('Installed apps not supported on this platform.', sessionid); } break; } case 'installedstoreapps': { - if(process.platform == 'win32'){ - var apps = require('win-info').installedStoreApps(); - if (apps[0]) { - sendConsoleText(JSON.stringify(apps,null,1)); - }; + if (process.platform == 'win32') { + try { + sendConsoleText('Fetching installed Store apps, please wait...', sessionid); + var isaPr = require('win-info').installedStoreApps(); + isaPr.sessionid = sessionid; + isaPr.then(function (apps) { + if (!apps || apps.length === 0) { sendConsoleText('No Store apps found.', this.sessionid); return; } + var lines = ['Installed Store Apps (' + apps.length + '):']; + for (var i = 0; i < apps.length; i++) { + var a = apps[i]; + var line = ' [' + (i + 1) + '] ' + a.name; + if (a.version) line += ' (v' + a.version + ')'; + if (a.publisher) line += ' - ' + a.publisher; + if (a.scope) line += ' [' + a.scope + ']'; + if (a.packageFullName) line += '\n Package: ' + a.packageFullName; + lines.push(line); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { + sendConsoleText('Module win-info error: ' + ex, sessionid); + } + } else { + sendConsoleText('Installed Store Apps not supported on this platform.', sessionid); + } + break; + } + case 'winupdates': { + if (process.platform == 'win32') { + try { + sendConsoleText('Checking for pending updates, this may take up to 60 seconds...', sessionid); + var wuPr = require('win-updates').getAvailableUpdates(); + wuPr.sessionid = sessionid; + wuPr.then(function (updates) { + if (updates.length === 0) { sendConsoleText('No pending updates.', this.sessionid); return; } + var lines = ['Pending Updates (' + updates.length + '):']; + for (var i = 0; i < updates.length; i++) { + lines.push(' [' + (i + 1) + '] ' + updates[i].title + (updates[i].kbArticleIDs.length ? ' (' + updates[i].kbArticleIDs.join(', ') + ')' : '') + (updates[i].severity ? ' [' + updates[i].severity + ']' : '')); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { sendConsoleText('win-updates error: ' + ex, sessionid); } + } else { sendConsoleText('Windows updates not supported on this platform.', sessionid); } + break; + } + case 'winupdatesinstalled': { + if (process.platform == 'win32') { + try { + sendConsoleText('Checking installed updates, this may take up to 60 seconds...', sessionid); + var wuiPr = require('win-updates').getInstalledUpdates(); + wuiPr.sessionid = sessionid; + wuiPr.then(function (updates) { + if (updates.length === 0) { sendConsoleText('No installed updates found.', this.sessionid); return; } + var lines = ['Installed Updates (' + updates.length + '):']; + for (var i = 0; i < updates.length; i++) { + lines.push(' [' + (i + 1) + '] ' + updates[i].title + (updates[i].kbArticleIDs.length ? ' (' + updates[i].kbArticleIDs.join(', ') + ')' : '')); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { sendConsoleText('win-updates error: ' + ex, sessionid); } + } else { sendConsoleText('Windows updates not supported on this platform.', sessionid); } + break; + } + case 'winupdateshistory': { + if (process.platform == 'win32') { + try { + sendConsoleText('Fetching update history, this may take up to 60 seconds...', sessionid); + var wuhPr = require('win-updates').getInstalledUpdateHistory(); + wuhPr.sessionid = sessionid; + wuhPr.then(function (updates) { + if (updates.length === 0) { sendConsoleText('No update history found.', this.sessionid); return; } + var lines = ['Update History (' + updates.length + '):']; + for (var i = 0; i < updates.length; i++) { + lines.push(' [' + (i + 1) + '] ' + updates[i].title + (updates[i].kbArticleIDs.length ? ' (' + updates[i].kbArticleIDs.join(', ') + ')' : '')); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { sendConsoleText('win-updates error: ' + ex, sessionid); } + } else { sendConsoleText('Windows updates not supported on this platform.', sessionid); } + break; + } + case 'winupdatesdedup': { + if (process.platform == 'win32') { + try { + sendConsoleText('Fetching de-duplicated update history, this may take up to 60 seconds...', sessionid); + var wudPr = require('win-updates').getInstalledUpdatesDeDuplicated(); + wudPr.sessionid = sessionid; + wudPr.then(function (updates) { + if (updates.length === 0) { sendConsoleText('No update history found.', this.sessionid); return; } + var lines = ['Installed Updates De-duplicated (' + updates.length + '):']; + for (var i = 0; i < updates.length; i++) { + lines.push(' [' + (i + 1) + '] ' + updates[i].title + (updates[i].kbArticleIDs.length ? ' (' + updates[i].kbArticleIDs.join(', ') + ')' : '')); + } + sendConsoleText(lines.join('\n'), this.sessionid); + }, function (e) { sendConsoleText('Error: ' + e, this.sessionid); }); + } catch (ex) { sendConsoleText('win-updates error: ' + ex, sessionid); } + } else { sendConsoleText('Windows updates not supported on this platform.', sessionid); } + break; + } + case 'uninstallapp': { + if (args['_'].length < 1) { + response = "Usage: uninstallapp "; + } else { + var base64Cmd = args['_'][0]; + var uninstallCmd = ''; + try { + var b = Buffer.from(base64Cmd, 'base64'); + var decoded = b.toString(); + var lc = decoded ? decoded.toLowerCase() : ''; + if (decoded && decoded.length > 0 && (lc.indexOf('msiexec') >= 0 || lc.indexOf('.exe') >= 0)) { + uninstallCmd = decoded; + } else { + uninstallCmd = base64Cmd; + } + } catch (e) { + uninstallCmd = base64Cmd; + } + if (!uninstallCmd || uninstallCmd.trim() === '' || uninstallCmd.trim() === '\\') { + response = JSON.stringify({ success: false, error: 'No valid uninstall command available' }); + break; + } + var logDir = (process.env['ProgramData'] || 'C:\\ProgramData') + '\\MeshAgent'; + try { if (!require('fs').existsSync(logDir)) { require('fs').mkdirSync(logDir); } } catch (e) { } + var logFile = logDir + '\\MeshAgent_Uninstall.log'; + var child_process = require('child_process'); + var cmdPath = process.env['windir'] + '\\system32\\cmd.exe'; + function writeLog(message, callback) { + try { + var timestamp = new Date().toISOString(); + var logLine = timestamp + ' | ' + message; + logLine = logLine.replace(/"/g, '""').replace(/&/g, '^&').replace(//g, '^>').replace(/\|/g, '^|'); + var logChild = child_process.execFile(cmdPath, ['cmd', '/c', 'echo ' + logLine + ' >> "' + logFile + '"'], { timeout: 5000 }); + logChild.on('exit', function() { + if (callback) callback(); + }); + } catch (e) { + if (callback) callback(); + } + } + try { + writeLog('UNINSTALL START - Command: ' + uninstallCmd.replace(/\\/g, '/')); + var originalCmd = uninstallCmd; + if (uninstallCmd.toLowerCase().indexOf('msiexec') >= 0) { + uninstallCmd = uninstallCmd.replace(/\/I\s*(\{[^}]+\})/gi, '/X $1'); + uninstallCmd = uninstallCmd.replace(/\/q[nbrf]?/gi, ''); + if (uninstallCmd.indexOf('/QN') < 0) { + uninstallCmd = uninstallCmd + ' /QN /norestart'; + } + } else { + if (uninstallCmd.toLowerCase().indexOf('/s') < 0) { + uninstallCmd = uninstallCmd + ' /S /silent /SILENT /VERYSILENT /quiet /norestart'; + } + } + uninstallCmd = uninstallCmd.replace(/\s+/g, ' ').trim(); + writeLog('UNINSTALL EXEC - Modified: ' + uninstallCmd.replace(/\\/g, '/')); + var child = child_process.execFile(cmdPath, ['cmd', '/c', uninstallCmd], { timeout: 300000 }); + child.stdout.str = ''; + child.stderr.str = ''; + child._sessionid = sessionid; + child._cmd = uninstallCmd; + child._originalCmd = originalCmd; + child._writeLog = writeLog; + child.stdout.on('data', function(c) { this.str += c.toString(); }); + child.stderr.on('data', function(c) { this.str += c.toString(); }); + child.on('exit', function(code) { + var success = (code === 0 || code === null || code === 3010); + var status = success ? 'SUCCESS' : 'FAILED'; + child._writeLog('UNINSTALL ' + status + ' - ExitCode: ' + code); + var result = { + success: success, + exitCode: code, + command: child._cmd + }; + if (child.stdout.str) result.stdout = child.stdout.str.trim().substring(0, 500); + if (child.stderr.str) result.stderr = child.stderr.str.trim().substring(0, 500); + sendConsoleText(JSON.stringify(result), child._sessionid); + }); + response = JSON.stringify({ status: 'Silent uninstall started', command: uninstallCmd }); + } catch (ex) { + writeLog('UNINSTALL ERROR - ' + ex.toString()); + response = JSON.stringify({ error: ex.toString() }); + } + } + break; + } + case 'uninstallstoreapp': { + if (args['_'].length < 1) { + response = "Usage: uninstallstoreapp "; + } else { + var packageName = args['_'][0]; + var logDir = (process.env['ProgramData'] || 'C:\\ProgramData') + '\\MeshAgent'; + try { if (!require('fs').existsSync(logDir)) { require('fs').mkdirSync(logDir); } } catch (e) { } + var logFile = logDir + '\\MeshAgent_StoreUninstall.log'; + try { + var psPath = (process.env['SystemRoot'] || 'C:\\Windows') + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; + var child_process = require('child_process'); + var safePackageName = packageName.replace(/'/g, "''"); + var completionMarker = '###DONE###' + Date.now(); + var child = child_process.execFile(psPath, ['powershell', '-NoProfile', '-NoLogo', '-Command', '-'], {}); + child.stdout.str = ''; + child.stderr.str = ''; + child._sessionid = sessionid; + child._packageName = packageName; + child._marker = completionMarker; + child._completed = false; + child._logFile = logFile; + child.stdout.on('data', function(c) { + this.str += c.toString(); + if (!child._completed && this.str.indexOf(child._marker) >= 0) { + child._completed = true; + var result = this.str.split(child._marker)[0].trim(); + if (result === 'NO_MATCH' || result === '') { + sendConsoleText(JSON.stringify({ + success: false, + error: 'No matching packages found', + package: child._packageName + }), child._sessionid); + } else if (result.indexOf('REMOVED') >= 0 || result.indexOf('DEPROVISIONED') >= 0) { + var items = result.split('|').filter(function(x) { return x.length > 0; }); + sendConsoleText(JSON.stringify({ + success: true, + message: 'Store app removed', + results: items + }), child._sessionid); + } else if (result.indexOf('FAILED') >= 0) { + sendConsoleText(JSON.stringify({ + success: false, + error: 'Removal failed', + details: result.split('|') + }), child._sessionid); + } else { + sendConsoleText(JSON.stringify({ output: result }), child._sessionid); + } + } + }); + child.stderr.on('data', function(c) { this.str += c.toString(); }); + var script = [ + "$LogFile = '" + logFile + "'", + "function Write-Log { param([string]$Message); $LogLine = \"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message\"; Add-Content -Path $LogFile -Value $LogLine -ErrorAction SilentlyContinue }", + "", + "Write-Log 'STORE UNINSTALL START: " + safePackageName + "'", + "Write-Log \"Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)\"", + "", + "$ErrorActionPreference = 'SilentlyContinue'", + "$results = @()", + "", + "# Packages suchen", + "$pkgs = Get-AppxPackage -AllUsers -Name '*" + safePackageName + "*'", + "Write-Log \"Found (AllUsers): $($pkgs.Count) packages\"", + "", + "if (-not $pkgs) {", + " $pkgs = Get-AppxPackage -Name '*" + safePackageName + "*'", + " Write-Log \"Found (CurrentUser): $($pkgs.Count) packages\"", + "}", + "", + "foreach ($p in $pkgs) {", + " Write-Log \"Removing: $($p.PackageFullName)\"", + " try {", + " Remove-AppxPackage -Package $p.PackageFullName -AllUsers -ErrorAction Stop", + " Write-Log 'SUCCESS: Removed with -AllUsers'", + " $results += 'REMOVED_ALLUSERS:' + $p.PackageFullName", + " } catch {", + " Write-Log \"FAILED -AllUsers: $($_.Exception.Message)\"", + " try {", + " Remove-AppxPackage -Package $p.PackageFullName -ErrorAction Stop", + " Write-Log 'SUCCESS: Removed'", + " $results += 'REMOVED:' + $p.PackageFullName", + " } catch {", + " Write-Log \"FAILED: $($_.Exception.Message)\"", + " $results += 'FAILED:' + $p.PackageFullName + ':' + $_.Exception.Message", + " }", + " }", + "}", + "", + "# Provisioned Packages entfernen", + "$prov = Get-AppxProvisionedPackage -Online 2>$null | Where-Object { $_.PackageName -like '*" + safePackageName + "*' }", + "Write-Log \"Found provisioned: $($prov.Count) packages\"", + "", + "foreach ($pr in $prov) {", + " Write-Log \"Deprovisioning: $($pr.DisplayName)\"", + " try {", + " Remove-AppxProvisionedPackage -Online -PackageName $pr.PackageName -ErrorAction Stop | Out-Null", + " Write-Log 'SUCCESS: Deprovisioned'", + " $results += 'DEPROVISIONED:' + $pr.DisplayName", + " } catch {", + " Write-Log \"FAILED: $($_.Exception.Message)\"", + " $results += 'DEPROV_FAILED:' + $pr.DisplayName", + " }", + "}", + "", + "if ($results.Count -eq 0) {", + " Write-Log 'NO_MATCH'", + " 'NO_MATCH'", + "} else {", + " $output = $results -join '|'", + " Write-Log \"RESULT: $output\"", + " $output", + "}", + "'" + completionMarker + "'", + "exit" + ].join("\r\n"); + child.stdin.write(script + "\r\n"); + setTimeout(function() { + if (!child._completed) { + sendConsoleText(JSON.stringify({ error: 'Timeout after 2 minutes' }), child._sessionid); + } + }, 120000); + response = JSON.stringify({ status: 'Store app removal started', package: packageName }); + } catch (ex) { + var logErr = new Date().toISOString() + ' - STORE UNINSTALL ERROR: ' + ex.toString() + '\r\n'; + try { require('fs').appendFileSync(logFile, logErr); } catch (e) { } + response = JSON.stringify({ error: ex.toString() }); + } } break; } @@ -6277,6 +7476,217 @@ function handleServerConnection(state) { broadcastToRegisteredApps({ cmd: 'serverstate', value: meshServerConnectionState, url: require('MeshAgent').ConnectedServer }); } +function getProbableMainLanIp() { + if (process.platform != 'win32') return null; + + function str(v) { + if (v == null) return ''; + try { return String(v); } catch (ex) { return ''; } + } + + function intVal(v, d) { + var x; + try { x = parseInt(str(v)); } catch (ex) { x = NaN; } + if (isNaN(x)) return d; + return x; + } + + function isIPv4(ip) { + var p, i, n; + + ip = str(ip); + if (ip == '') return false; + if (ip == '0.0.0.0') return false; + if (ip.indexOf(':') >= 0) return false; + if (ip.indexOf('169.254.') == 0) return false; + if (ip.indexOf('127.') == 0) return false; + + p = ip.split('.'); + if (p.length != 4) return false; + + for (i = 0; i < 4; i++) { + if (p[i] == '') return false; + n = parseInt(p[i]); + if (isNaN(n)) return false; + if ((n < 0) || (n > 255)) return false; + } + + return true; + } + + function hasAnyValue(v) { + var i; + + if (v == null) return false; + + if (typeof v == 'string') { + return (v != ''); + } + + if (typeof v == 'number') { + return true; + } + + // Duktape/WMI may expose arrays or array-like objects. + try { + if (v.length != null) { + for (i = 0; i < v.length; i++) { + if (str(v[i]) != '') return true; + } + return false; + } + } catch (ex) { } + + return (str(v) != ''); + } + + function firstIPv4(v) { + var i, s; + + if (v == null) return null; + + if (typeof v == 'string') { + return isIPv4(v) ? v : null; + } + + try { + if (v.length != null) { + for (i = 0; i < v.length; i++) { + s = str(v[i]); + if (isIPv4(s)) return s; + } + } + } catch (ex) { } + + s = str(v); + if (isIPv4(s)) return s; + + return null; + } + + function isUpAdapter(a) { + var op, media; + + if (a == null) return true; + + // InterfaceOperationalStatus commonly has 1 = Up. + // But do not hard-fail unknown values; WMI support varies. + op = intVal(a.InterfaceOperationalStatus, -1); + if ((op != -1) && (op != 1)) return false; + + // MediaConnectState commonly has 1 = Connected. + media = intVal(a.MediaConnectState, -1); + if ((media != -1) && (media != 1)) return false; + + return true; + } + + function adapterPriority(a) { + var pm, medium, link, hardware, connector; + + if (a == null) return 20; + + pm = intVal(a.NdisPhysicalMedium, -1); + medium = intVal(a.NdisMedium, -1); + link = intVal(a.LinkTechnology, -1); + + hardware = (str(a.HardwareInterface).toLowerCase() == 'true'); + connector = (str(a.ConnectorPresent).toLowerCase() == 'true'); + + /* + MSFT_NetAdapter is documented in ROOT\StandardCimv2. + Useful physical media values include: + 14 = 802.3 / Ethernet + 1 = Wireless LAN + 9 = Native 802.11 + + These numeric values avoid matching adapter names such as + "Wi-Fi", "WLAN", or "Wireless". + */ + + if (pm == 14) return 0; // wired Ethernet + if ((medium == 0) && connector) return 1; // 802.3-style fallback + if ((link == 2) && hardware) return 2; // Ethernet-ish fallback + + if ((pm == 1) || (pm == 9)) return 10; // Wi-Fi / WLAN + if (link == 11) return 11; // wireless fallback + + return 20; // VPN, virtual, tunnel, unknown + } + + try { + var wmi = require('win-wmi-fixed'); + var configs; + var adapters; + var adapterByIndex = {}; + var candidates = []; + var i, cfg, a, ip, gw, idx, metric; + + configs = wmi.query( + 'ROOT\\CIMV2', + 'SELECT InterfaceIndex,IPAddress,DefaultIPGateway,IPConnectionMetric FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = True', + ['InterfaceIndex', 'IPAddress', 'DefaultIPGateway', 'IPConnectionMetric'] + ); + + if ((configs == null) || (configs.length == null) || (configs.length == 0)) { + return null; + } + + try { + adapters = wmi.query( + 'ROOT\\StandardCimv2', + 'SELECT InterfaceIndex,NdisPhysicalMedium,NdisMedium,LinkTechnology,HardwareInterface,ConnectorPresent,InterfaceOperationalStatus,MediaConnectState FROM MSFT_NetAdapter', + ['InterfaceIndex', 'NdisPhysicalMedium', 'NdisMedium', 'LinkTechnology', 'HardwareInterface', 'ConnectorPresent', 'InterfaceOperationalStatus', 'MediaConnectState'] + ); + + if ((adapters != null) && (adapters.length != null)) { + for (i = 0; i < adapters.length; i++) { + idx = intVal(adapters[i].InterfaceIndex, -1); + if (idx >= 0) adapterByIndex['i' + idx] = adapters[i]; + } + } + } catch (ex2) { + adapters = null; + } + + for (i = 0; i < configs.length; i++) { + cfg = configs[i]; + + gw = cfg.DefaultIPGateway; + if (!hasAnyValue(gw)) continue; + + ip = firstIPv4(cfg.IPAddress); + if (ip == null) continue; + + idx = intVal(cfg.InterfaceIndex, -1); + a = adapterByIndex['i' + idx]; + + if (!isUpAdapter(a)) continue; + + metric = intVal(cfg.IPConnectionMetric, 999999); + + candidates[candidates.length] = { + ip: ip, + idx: idx, + pri: adapterPriority(a), + metric: metric + }; + } + + if (candidates.length == 0) return null; + + candidates.sort(function (x, y) { + if (x.pri != y.pri) return x.pri - y.pri; + if (x.metric != y.metric) return x.metric - y.metric; + return x.idx - y.idx; + }); + + return candidates[0].ip; + } catch (ex) { + return null; + } +} + // Update the server with the latest network interface information var sendNetworkUpdateNagleTimer = null; function sendNetworkUpdateNagle() { if (sendNetworkUpdateNagleTimer != null) { clearTimeout(sendNetworkUpdateNagleTimer); sendNetworkUpdateNagleTimer = null; } sendNetworkUpdateNagleTimer = setTimeout(sendNetworkUpdate, 5000); } @@ -6288,7 +7698,7 @@ function sendNetworkUpdate(force) { var netInfo = { netif2: require('os').networkInterfaces() }; if (process.platform == 'win32') { try { - var ret = require('win-wmi').query('ROOT\\CIMV2', 'SELECT InterfaceIndex,NetConnectionID,Speed FROM Win32_NetworkAdapter', ['InterfaceIndex','NetConnectionID','Speed']); + var ret = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT InterfaceIndex,NetConnectionID,Speed FROM Win32_NetworkAdapter', ['InterfaceIndex','NetConnectionID','Speed']); if (ret[0]) { var speedMap = {}; for (var i = 0; i < ret.length; i++) speedMap[ret[i].InterfaceIndex] = ret[i].Speed; @@ -6302,6 +7712,13 @@ function sendNetworkUpdate(force) { } } } catch(ex) { } + + try { + var probableMainLanIp = getProbableMainLanIp(); + if (probableMainLanIp != null) { + netInfo.probable_main_lan_ip = probableMainLanIp; + } + } catch (ex) { } } else if (process.platform == 'linux') { var adapterNames = Object.keys(netInfo.netif2); for (var i = 0; i < adapterNames.length; i++) { @@ -6327,67 +7744,101 @@ function sendNetworkUpdate(force) { } // Called periodically to check if we need to send updates to the server +var sendPeriodicServerUpdateInProgress = false; function sendPeriodicServerUpdate(flags, force) { if (meshServerConnectionState == 0) return; // Not connected to server, do nothing. - if (!flags) { flags = 0xFFFFFFFF; } - if (!force) { force = false; } - - // If we have a connected MEI, get Intel ME information - if ((flags & 1) && (amt != null) && (amt.state == 2)) { - delete meshCoreObj.intelamt; - amt.getMeiState(9, function (meinfo) { - meshCoreObj.intelamt = meinfo; - meshCoreObj.intelamt.microlms = amt.lmsstate; - meshCoreObjChanged(); - }); - } - // Update network information - if (flags & 2) { sendNetworkUpdateNagle(false); } + // Defensive guard against re-entrancy. The info collectors below are currently + // non-blocking (Linux av()/firewall() are async; Windows uses native WMI), so + // nothing here pumps the event loop and this guard never actually fires. It's + // kept as a safety net: if a future collector re-introduces a blocking + // child.waitExit() (which runs a nested event loop), a timer firing inside it + // could re-enter and throw 'waitExit() already in progress'. Skipping the + // overlapping run avoids that. + if (sendPeriodicServerUpdateInProgress) return; + sendPeriodicServerUpdateInProgress = true; + try { + if (!flags) { flags = 0xFFFFFFFF; } + if (!force) { force = false; } + + // If we have a connected MEI, get Intel ME information + if ((flags & 1) && (amt != null) && (amt.state == 2)) { + delete meshCoreObj.intelamt; + amt.getMeiState(9, function (meinfo) { + meshCoreObj.intelamt = meinfo; + meshCoreObj.intelamt.microlms = amt.lmsstate; + meshCoreObjChanged(); + }); + } + + // Update network information + if (flags & 2) { sendNetworkUpdateNagle(false); } - // Update anti-virus information - if ((flags & 4) && (process.platform == 'win32')) { - // Windows Command: "wmic /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct get /FORMAT:CSV" - try { meshCoreObj.av = require('win-info').av(); meshCoreObjChanged(); } catch (ex) { av = null; } // Antivirus - //if (process.platform == 'win32') { try { meshCoreObj.pr = require('win-info').pendingReboot(); meshCoreObjChanged(); } catch (ex) { meshCoreObj.pr = null; } } // Pending reboot - } - if (process.platform == 'win32') { - if (require('MeshAgent')._securitycenter == null) { + // Update anti-virus information + if ((flags & 4) && (process.platform == 'win32')) { + // Windows Command: "wmic /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct get /FORMAT:CSV" + try { meshCoreObj.av = require('win-info').av(); meshCoreObjChanged(); } catch (ex) { av = null; } // Antivirus + //if (process.platform == 'win32') { try { meshCoreObj.pr = require('win-info').pendingReboot(); meshCoreObjChanged(); } catch (ex) { meshCoreObj.pr = null; } } // Pending reboot + } + // Update Linux AV/Firewall information. av()/firewall() are asynchronous (they + // spawn child processes), so they don't block this periodic update with a nested + // waitExit() loop. Results are applied in the callbacks and flushed via + // meshCoreObjChanged(). + if ((flags & 4) && (process.platform == 'linux')) { try { - require('MeshAgent')._securitycenter = require('win-securitycenter').status(); - meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC) - require('win-securitycenter').on('changed', function () { - require('MeshAgent')._securitycenter = require('win-securitycenter').status(); - meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC) - require('MeshAgent').SendCommand({ action: 'coreinfo', wsc: require('MeshAgent')._securitycenter }); + require('linux-info').av(function (avResult) { + try { + var lsc = {}; + if (avResult && avResult.length > 0) { meshCoreObj.av = avResult; lsc.antiVirus = 'OK'; } + require('linux-info').firewall(function (fwResult) { + try { + if (fwResult && fwResult.installed) { lsc.firewall = fwResult.enabled ? 'OK' : 'BAD'; } + if (Object.keys(lsc).length > 0) { meshCoreObj.lsc = lsc; meshCoreObjChanged(); } + } catch (ex) { } + }); + } catch (ex) { } }); } catch (ex) { } } - // Get Defender Information - try { - meshCoreObj.defender = require('win-info').defender(); - meshCoreObjChanged(); - } catch (ex) { } + if (process.platform == 'win32') { + if (require('MeshAgent')._securitycenter == null) { + try { + require('MeshAgent')._securitycenter = require('win-securitycenter').status(); + meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC) + require('win-securitycenter').on('changed', function () { + require('MeshAgent')._securitycenter = require('win-securitycenter').status(); + meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC) + require('MeshAgent').SendCommand({ action: 'coreinfo', wsc: require('MeshAgent')._securitycenter }); + }); + } catch (ex) { } + } - // Calculate Windows Idle Time - try { - require('win-deskutils').idle.getSecondsAllSessions().then(function (seconds) { - meshCoreObj.idletime = seconds; + // Get Defender Information + try { + meshCoreObj.defender = require('win-info').defender(); meshCoreObjChanged(); - }); - } catch (ex) { sendConsoleText('Error getting idle time: ' + ex.toString());} - } + } catch (ex) { } - // Send available data right now - if (force) { - meshCoreObj = sortObjRec(meshCoreObj); - var x = JSON.stringify(meshCoreObj); - if (x != LastPeriodicServerUpdate) { - LastPeriodicServerUpdate = x; - mesh.SendCommand(meshCoreObj); + // Calculate Windows Idle Time + try { + require('win-deskutils').idle.getSecondsAllSessions().then(function (seconds) { + meshCoreObj.idletime = seconds; + meshCoreObjChanged(); + }); + } catch (ex) { sendConsoleText('Error getting idle time: ' + ex.toString());} } - } + + // Send available data right now + if (force) { + meshCoreObj = sortObjRec(meshCoreObj); + var x = JSON.stringify(meshCoreObj); + if (x != LastPeriodicServerUpdate) { + LastPeriodicServerUpdate = x; + mesh.SendCommand(meshCoreObj); + } + } + } finally { sendPeriodicServerUpdateInProgress = false; } } // Sort the names in an object @@ -6395,17 +7846,17 @@ function sortObject(obj) { return Object.keys(obj).sort().reduce(function(a, v) // Fix the incoming data and cut down how much data we use function cleanGetBitLockerVolumeInfo(volumes) { + // Keep the raw codes info and let the view convert to strings for (var i in volumes) { const v = volumes[i]; if (typeof v.size == 'string') { v.size = parseInt(v.size); } if (typeof v.sizeremaining == 'string') { v.sizeremaining = parseInt(v.sizeremaining); } if (v.identifier == '') { delete v.identifier; } if (v.name == '') { delete v.name; } - if (v.removable != true) { delete v.removable; } - if (v.cdrom != true) { delete v.cdrom; } - if (v.protectionStatus == 'On') { v.protectionStatus = true; } else { delete v.protectionStatus; } - if (v.volumeStatus == 'FullyDecrypted') { delete v.volumeStatus; } if (v.recoveryPassword == '') { delete v.recoveryPassword; } + if (v.volumeStatus === 0) { delete v.volumeStatus; } + if (v.encryptionMethod === 0) { delete v.encryptionMethod; } + if (v.protectionStatus === 0) { delete v.protectionStatus; } } return sortObject(volumes); } @@ -6444,4 +7895,3 @@ function onWebSocketUpgrade(response, s, head) { mesh.AddCommandHandler(handleServerCommand); mesh.AddConnectHandler(handleServerConnection); - diff --git a/agents/modules_meshcmd/sysinfo.js b/agents/modules_meshcmd/sysinfo.js index 611a7b1af6..27c825c5e0 100644 --- a/agents/modules_meshcmd/sysinfo.js +++ b/agents/modules_meshcmd/sysinfo.js @@ -226,7 +226,7 @@ function windows_thermals() { var ret = []; try { - ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']); + ret = require('win-wmi-fixed').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']); if (ret[0]) { for (var i = 0; i < ret.length; ++i) { ret[i]['CurrentTemperature'] = ((parseFloat(ret[i]['CurrentTemperature']) / 10) - 273.15).toFixed(2); diff --git a/agents/modules_meshcore/computer-identifiers.js b/agents/modules_meshcore/computer-identifiers.js index cfee592437..f168a2c6e0 100644 --- a/agents/modules_meshcore/computer-identifiers.js +++ b/agents/modules_meshcore/computer-identifiers.js @@ -70,55 +70,58 @@ function linux_identifiers() var ret = {}; var values = {}; var child = null; - - if (!require('fs').existsSync('/sys/class/dmi/id')) { - if (require('fs').existsSync('/sys/firmware/devicetree/base/model')) { - if (require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim().startsWith('Raspberry')) { - identifiers['board_vendor'] = 'Raspberry Pi'; - identifiers['board_name'] = require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim(); - identifiers['board_serial'] = require('fs').readFileSync('/sys/firmware/devicetree/base/serial-number').toString().trim(); - const memorySlots = []; - child = require('child_process').execFile('/bin/sh', ['sh']); - child.stdout.str = ''; child.stdout.on('data', dataHandler); - child.stdin.write('vcgencmd get_mem arm && vcgencmd get_mem gpu\nexit\n'); - child.waitExit(); - try { - const lines = child.stdout.str.trim().split('\n'); - if (lines.length == 2) { - memorySlots.push({ Locator: "ARM Memory", Size: lines[0].split('=')[1].trim() }) - memorySlots.push({ Locator: "GPU Memory", Size: lines[1].split('=')[1].trim() }) - ret.memory = { Memory_Device: memorySlots }; - } - } catch (xx) { } + try { + identifiers['bios_vendor'] = 'Unknown'; + if (!require('fs').existsSync('/sys/class/dmi/id')) { + if (require('fs').existsSync('/etc/wsl.conf')) { + identifiers['bios_vendor'] = 'Microsoft'; + identifiers['bios_version'] = 'WSL'; } else { - throw('Unknown board'); + if (require('fs').existsSync('/sys/firmware/devicetree/base/model')) { + if (require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim().startsWith('Raspberry')) { + identifiers['board_vendor'] = 'Raspberry Pi'; + identifiers['board_name'] = require('fs').readFileSync('/sys/firmware/devicetree/base/model').toString().trim(); + identifiers['board_serial'] = require('fs').readFileSync('/sys/firmware/devicetree/base/serial-number').toString().trim(); + const memorySlots = []; + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', dataHandler); + child.stdin.write('vcgencmd get_mem arm && vcgencmd get_mem gpu\nexit\n'); + child.waitExit(); + try { + const lines = child.stdout.str.trim().split('\n'); + if (lines.length == 2) { + memorySlots.push({ Locator: "ARM Memory", Size: lines[0].split('=')[1].trim() }) + memorySlots.push({ Locator: "GPU Memory", Size: lines[1].split('=')[1].trim() }) + ret.memory = { Memory_Device: memorySlots }; + } + } catch (xx) { } + } + } } } else { - throw ('this platform does not have DMI statistics'); - } - } else { - var entries = require('fs').readdirSync('/sys/class/dmi/id'); - for (var i in entries) { - if (require('fs').statSync('/sys/class/dmi/id/' + entries[i]).isFile()) { - try { - ret[entries[i]] = require('fs').readFileSync('/sys/class/dmi/id/' + entries[i]).toString().trim(); - } catch(z) { } - if (ret[entries[i]] == 'None') { delete ret[entries[i]]; } + var entries = require('fs').readdirSync('/sys/class/dmi/id'); + for (var i in entries) { + if (require('fs').statSync('/sys/class/dmi/id/' + entries[i]).isFile()) { + try { + ret[entries[i]] = require('fs').readFileSync('/sys/class/dmi/id/' + entries[i]).toString().trim(); + } catch(z) { } + if (ret[entries[i]] == 'None') { delete ret[entries[i]]; } + } } + entries = null; + + identifiers['bios_date'] = ret['bios_date']; + identifiers['bios_vendor'] = ret['bios_vendor']; + identifiers['bios_version'] = ret['bios_version']; + identifiers['bios_serial'] = ret['product_serial']; + identifiers['board_name'] = ret['board_name']; + identifiers['board_serial'] = ret['board_serial']; + identifiers['board_vendor'] = ret['board_vendor']; + identifiers['board_version'] = ret['board_version']; + identifiers['product_uuid'] = ret['product_uuid']; + identifiers['product_name'] = ret['product_name']; } - entries = null; - - identifiers['bios_date'] = ret['bios_date']; - identifiers['bios_vendor'] = ret['bios_vendor']; - identifiers['bios_version'] = ret['bios_version']; - identifiers['bios_serial'] = ret['product_serial']; - identifiers['board_name'] = ret['board_name']; - identifiers['board_serial'] = ret['board_serial']; - identifiers['board_vendor'] = ret['board_vendor']; - identifiers['board_version'] = ret['board_version']; - identifiers['product_uuid'] = ret['product_uuid']; - identifiers['product_name'] = ret['product_name']; - } + } catch (e) { console.log(e && e.message ? e.message : e); } // BIOS Mode try { @@ -161,6 +164,26 @@ function linux_identifiers() } } catch (xx) { } + // Kernel info + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', dataHandler); + child.stdin.write('uname -r\nexit\n'); + child.waitExit(); + try { ret['kernel_release'] = child.stdout.str.trim(); } catch (xx) { } + if (ret['kernel_release'].indexOf('-WSL2') !== -1) { identifiers['bios_version'] = 'WSL2'; } + + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', dataHandler); + child.stdin.write('uname -v\nexit\n'); + child.waitExit(); + try { ret['kernel_build'] = child.stdout.str.trim(); } catch (xx) { } + + child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', dataHandler); + child.stdin.write('uname -m\nexit\n'); + child.waitExit(); + try { ret['arch'] = child.stdout.str.trim(); } catch (xx) { } + // Fetch GPU info child = require('child_process').execFile('/bin/sh', ['sh']); child.stdout.str = ''; child.stdout.on('data', dataHandler); @@ -422,9 +445,12 @@ function linux_identifiers() ]; const thedata = {}; for (var x in filesToRead) { - try { - const content = require('fs').readFileSync('/sys/class/power_supply/' + batteries[i] + '/' + filesToRead[x]).toString().trim(); - thedata[filesToRead[x]] = /^\d+$/.test(content) ? parseInt(content, 10) : content; + try { + const filePath = '/sys/class/power_supply/' + batteries[i] + '/' + filesToRead[x]; + if(require('fs').existsSync(filePath)) { + const content = require('fs').readFileSync(filePath).toString().trim(); + thedata[filesToRead[x]] = /^\d+$/.test(content) ? parseInt(content, 10) : content; + } } catch (err) { } } if (Object.keys(thedata).length === 0) continue; // No data read, skip @@ -497,7 +523,7 @@ function windows_identifiers() ret['identifiers'] = {}; - var values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_Bios", ['ReleaseDate', 'Manufacturer', 'SMBIOSBIOSVersion', 'SerialNumber']); + var values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_Bios", ['ReleaseDate', 'Manufacturer', 'SMBIOSBIOSVersion', 'SerialNumber']); if(values[0]){ ret['identifiers']['bios_date'] = values[0]['ReleaseDate']; ret['identifiers']['bios_vendor'] = values[0]['Manufacturer']; @@ -506,7 +532,7 @@ function windows_identifiers() } ret['identifiers']['bios_mode'] = 'Legacy'; - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_BaseBoard", ['Product', 'SerialNumber', 'Manufacturer', 'Version']); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_BaseBoard", ['Product', 'SerialNumber', 'Manufacturer', 'Version']); if(values[0]){ ret['identifiers']['board_name'] = values[0]['Product']; ret['identifiers']['board_serial'] = values[0]['SerialNumber']; @@ -514,13 +540,13 @@ function windows_identifiers() ret['identifiers']['board_version'] = values[0]['Version']; } - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_ComputerSystemProduct", ['UUID', 'Name']); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_ComputerSystemProduct", ['UUID', 'Name']); if(values[0]){ ret['identifiers']['product_uuid'] = values[0]['UUID']; ret['identifiers']['product_name'] = values[0]['Name']; } - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_SystemEnclosure", ['SerialNumber', 'SMBIOSAssetTag', 'Manufacturer']); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_SystemEnclosure", ['SerialNumber', 'SMBIOSAssetTag', 'Manufacturer']); if(values[0]){ ret['identifiers']['chassis_serial'] = values[0]['SerialNumber']; ret['identifiers']['chassis_assettag'] = values[0]['SMBIOSAssetTag']; @@ -529,13 +555,13 @@ function windows_identifiers() trimIdentifiers(ret.identifiers); - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_PhysicalMemory"); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_PhysicalMemory"); if(values[0]){ trimResults(values); ret.windows.memory = values; } - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_OperatingSystem"); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_OperatingSystem"); if(values[0]){ trimResults(values); ret.windows.osinfo = values[0]; @@ -560,17 +586,17 @@ function windows_identifiers() } } - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_Processor", ['Caption', 'DeviceID', 'Manufacturer', 'MaxClockSpeed', 'Name', 'SocketDesignation']); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_Processor", ['Caption', 'DeviceID', 'Manufacturer', 'MaxClockSpeed', 'Name', 'SocketDesignation']); if(values[0]){ ret.windows.cpu = values; } - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_VideoController", ['Name', 'CurrentHorizontalResolution', 'CurrentVerticalResolution']); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_VideoController", ['Name', 'CurrentHorizontalResolution', 'CurrentVerticalResolution']); if(values[0]){ ret.windows.gpu = values; } - values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_DiskDrive", ['Caption', 'DeviceID', 'Model', 'Partitions', 'Size', 'Status']); + values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_DiskDrive", ['Caption', 'DeviceID', 'Model', 'Partitions', 'Size', 'Status']); if(values[0]){ ret.windows.drives = values; } @@ -594,7 +620,7 @@ function windows_identifiers() // Windows TPM IntToStr = function (v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); }; try { - values = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftTpm', "SELECT * FROM Win32_Tpm", ['IsActivated_InitialValue','IsEnabled_InitialValue','IsOwned_InitialValue','ManufacturerId','ManufacturerVersion','SpecVersion']); + values = require('win-wmi-fixed').query('ROOT\\CIMV2\\Security\\MicrosoftTpm', "SELECT * FROM Win32_Tpm", ['IsActivated_InitialValue','IsEnabled_InitialValue','IsOwned_InitialValue','ManufacturerId','ManufacturerVersion','SpecVersion']); if(values[0]) { ret.tpm = { SpecVersion: values[0].SpecVersion.split(",")[0], @@ -643,15 +669,15 @@ function windows_identifiers() } return result; } - values = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryCycleCount",['InstanceName','CycleCount']); - var values2 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryFullChargedCapacity",['InstanceName','FullChargedCapacity']); - var values3 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryRuntime",['InstanceName','EstimatedRuntime']); - var values4 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryStaticData",['InstanceName','Chemistry','DesignedCapacity','DeviceName','ManufactureDate','ManufactureName','SerialNumber']); + values = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryCycleCount",['InstanceName','CycleCount']); + var values2 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryFullChargedCapacity",['InstanceName','FullChargedCapacity']); + var values3 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryRuntime",['InstanceName','EstimatedRuntime']); + var values4 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryStaticData",['InstanceName','Chemistry','DesignedCapacity','DeviceName','ManufactureDate','ManufactureName','SerialNumber']); for (i = 0; i < values4.length; ++i) { if (values4[i].Chemistry) { values4[i].Chemistry = IntToStrLE(parseInt(values4[i].Chemistry)); } if (values4[i].ManufactureDate) { if (values4[i].ManufactureDate.indexOf('*****') != -1) delete values4[i].ManufactureDate; } } - var values5 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryStatus",['InstanceName','ChargeRate','Charging','DischargeRate','Discharging','RemainingCapacity','Voltage']); + var values5 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryStatus",['InstanceName','ChargeRate','Charging','DischargeRate','Discharging','RemainingCapacity','Voltage']); var values6 = []; if (values2.length > 0 && values4.length > 0) { for (i = 0; i < values2.length; ++i) { @@ -885,7 +911,7 @@ function win_chassisType() function win_systemType() { try { - var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT PCSystemType FROM Win32_ComputerSystem', ['PCSystemType']); + var tokens = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT PCSystemType FROM Win32_ComputerSystem', ['PCSystemType']); if (tokens[0]) { return (parseInt(tokens[0]['PCSystemType'])); } else { @@ -1052,6 +1078,7 @@ module.exports.isVM = function isVM() } } + if (id.identifiers.bios_vendor == 'Microsoft' && id.identifiers.bios_version.slice(0,3) === 'WSL') { ret = true; } if (!ret) { ret = this.isDocker(); } return (ret); diff --git a/agents/modules_meshcore/linux-info.js b/agents/modules_meshcore/linux-info.js new file mode 100644 index 0000000000..eae4e037dd --- /dev/null +++ b/agents/modules_meshcore/linux-info.js @@ -0,0 +1,288 @@ + +var promise = require('promise'); + +function dataHandler(data) { this.str += data.toString(); } + +// Shell preamble that sets $TO to a 'timeout N' prefix when the 'timeout' utility +// is present (busybox/coreutils), or empty otherwise. Killing /bin/sh does not +// stop a reparented grandchild (e.g. a hung clamscan); 'timeout' does. +function shellTimeout(seconds) { return 'TO=""; command -v timeout >/dev/null 2>&1 && TO="timeout ' + seconds + '"; '; } + +// Run a short shell script WITHOUT blocking the event loop: cb(trimmedStdout) is +// called when the child exits or the timeout elapses. This never calls waitExit() +// (whose nested event loop can re-enter from another timer as 'waitExit() already +// in progress' when a command hangs). The child is killed on overrun, and +// shellTimeout()'s 'timeout' prefix stops a hung grandchild. +function runShellAsync(script, timeout, cb) { + var done = false; + function finish(child, out) { + if (done) { return; } + done = true; + if (child) { + if (child._to) { try { clearTimeout(child._to); } catch (ex) { } } + try { child.kill(); } catch (ex) { } + } + cb((out || '').trim()); + } + try { + var child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; child.stdout.on('data', dataHandler); + child.stderr.on('data', function () { }); + child.on('exit', function () { finish(this, this.stdout.str); }); + child._to = setTimeout(function () { finish(child, child.stdout.str); }, timeout); + child.stdin.write(script + '\nexit\n'); + } catch (ex) { finish(null, ''); } +} + +// Asynchronous so it never blocks the caller with a waitExit() nested loop. Calls +// callback(arrayOrEmpty). +function av(callback) +{ + var fs = require('fs'); + + // Find the clamscan binary and use its absolute path (don't depend on PATH, + // the agent environment may not include the binary's directory). + var clamBinaries = ['/usr/bin/clamscan', '/usr/local/bin/clamscan', '/bin/clamscan']; + var clamPath = null; + for (var i = 0; i < clamBinaries.length; ++i) { + try { if (fs.existsSync(clamBinaries[i])) { clamPath = clamBinaries[i]; break; } } catch (ex) { } + } + if (clamPath == null) { callback([]); return; } + + var status = { product: 'ClamAV', enabled: false, updated: false }; + + // Virus database date: newest mtime of the ClamAV signature files. Pure JS, so + // it needs no subprocess and can't hang on busybox-vs-coreutils tool differences. + try { + var newest = 0; + var dbFiles = ['/var/lib/clamav/main.cvd', '/var/lib/clamav/main.cld']; + for (var k = 0; k < dbFiles.length; ++k) { + try { var st = fs.statSync(dbFiles[k]); var t = (st && st.mtime) ? st.mtime.getTime() : 0; if (t > newest) { newest = t; } } catch (ex) { } + } + if (newest > 0) { + var d = new Date(newest); + status.definitionDate = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2); + status.updated = true; + } + } catch (ex) { } + + // Get the ClamAV version, then whether clamd is running, then return. Both run as + // non-blocking children, so a hung command can't stall the agent or nest waitExit(). + runShellAsync(shellTimeout(4) + '$TO "' + clamPath + '" --version 2>/dev/null', 4000, function (verOut) { + var v = verOut.split('\n')[0].trim(); + if (v) { status.product = v; } + // systemd via 'is-active'; otherwise a direct process check (pgrep). The old + // 'rc-service clamd status' path could block for seconds on Alpine/OpenRC. + runShellAsync( + shellTimeout(4) + + 'if command -v systemctl >/dev/null 2>&1; then ' + + 'if $TO systemctl is-active --quiet clamav-daemon 2>/dev/null || $TO systemctl is-active --quiet clamd 2>/dev/null; then echo active; else echo inactive; fi; ' + + 'else ' + + 'pgrep -x clamd >/dev/null 2>&1 && echo active || echo inactive; ' + + 'fi', 4000, function (svcOut) { + status.enabled = (svcOut.split('\n')[0].trim() === 'active'); + callback([status]); + }); + }); +} + +// Asynchronous so it never blocks the caller with a waitExit() nested loop. Calls +// callback(objectOrNull). +function firewall(callback) +{ + var fs = require('fs'); + + // Find the ufw binary and use its absolute path (don't depend on PATH). + var ufwBinaries = ['/usr/sbin/ufw', '/sbin/ufw', '/usr/bin/ufw']; + var ufwPath = null; + for (var i = 0; i < ufwBinaries.length; ++i) { + try { if (fs.existsSync(ufwBinaries[i])) { ufwPath = ufwBinaries[i]; break; } } catch (ex) { } + } + if (ufwPath == null) { callback(null); return; } + + runShellAsync(shellTimeout(4) + '$TO "' + ufwPath + '" status 2>/dev/null', 4000, function (out) { + callback({ + product: 'UFW', + installed: true, + enabled: /^status:\s+active$/i.test(out.split('\n')[0]) + }); + }); +} + +function packages() { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + var fs = require('fs'); + + // Run a shell script without blocking the event loop, capped at 30s; cb(stdout). + function runSh(script, cb) { runShellAsync(shellTimeout(25) + '$TO ' + script, 30000, cb); } + + function dirExists(path) { + try { return fs.existsSync(path); } catch (ex) { return false; } + } + + function binExists(paths) { + for (var i = 0; i < paths.length; ++i) { + try { if (fs.existsSync(paths[i])) return true; } catch (ex) { } + } + return false; + } + + // ---------- dpkg (Debian/Ubuntu) ---------- + function collectDpkg(cb) { + if (!binExists(['/usr/bin/dpkg-query', '/bin/dpkg-query']) || !dirExists('/var/lib/dpkg/info')) { cb([]); return; } + + runSh('dpkg-query -W -f=\'${binary:Package}\\t${Version}\\t${Architecture}\\t${db:Status-Abbrev}\\t${Maintainer}\\n\' 2>/dev/null', function (out) { + if (!out) { cb([]); return; } + runSh('find /var/lib/dpkg/info -name \'*.list\' -printf \'%f\\t%TY-%Tm-%Td\\n\' 2>/dev/null', function (dateOut) { + var dateMap = {}; + if (dateOut) { + dateOut.split('\n').forEach(function (line) { + var p = line.split('\t'); + if (p.length >= 2) { dateMap[p[0].replace(/\.list$/, '')] = p[1]; } + }); + } + + var results = []; + out.split('\n').forEach(function (line) { + var p = line.split('\t'); + if (p.length < 4) return; + if (p[3].indexOf('ii') !== 0) return; + results.push({ name: p[0], version: p[1], arch: p[2], publisher: p[4] || '', date: dateMap[p[0]] || '', location: 'dpkg' }); + }); + cb(results); + }); + }); + } + + // ---------- rpm (RHEL/Fedora/CentOS/SUSE) ---------- + function collectRpm(cb) { + if (!binExists(['/usr/bin/rpm', '/bin/rpm']) || (!dirExists('/var/lib/rpm') && !dirExists('/usr/lib/sysimage/rpm'))) { cb([]); return; } + + runSh('rpm -qa --qf \'%{NAME}\\t%{VERSION}\\t%{RELEASE}\\t%{ARCH}\\t%{VENDOR}\\t%{INSTALLTIME:date}\\n\' 2>/dev/null', function (out) { + if (!out) { cb([]); return; } + var results = []; + out.split('\n').forEach(function (line) { + var p = line.split('\t'); + if (p.length < 5 || !p[0]) return; + results.push({ name: p[0], version: p[1] + (p[2] ? '-' + p[2] : ''), arch: p[3], publisher: p[4] || '', date: p[5] || '', location: 'rpm' }); + }); + cb(results); + }); + } + + // ---------- pacman (Arch/Manjaro) ---------- + function collectPacman(cb) { + if (!binExists(['/usr/bin/pacman', '/bin/pacman']) || !dirExists('/var/lib/pacman/local')) { cb([]); return; } + + runSh('pacman -Qi 2>/dev/null', function (out) { + if (!out) { cb([]); return; } + var results = []; + var current = {}; + out.split('\n').forEach(function (line) { + var m = line.match(/^([A-Za-z ]+?)\s*:\s(.+)$/); + if (m) { + current[m[1].trim()] = m[2].trim(); + } else if (!line.trim() && current['Name']) { + results.push({ name: current['Name'], version: current['Version'] || '', arch: current['Architecture'] || '', publisher: current['Packager'] || '', date: current['Install Date'] || '', location: 'pacman' }); + current = {}; + } + }); + if (current['Name']) { + results.push({ name: current['Name'], version: current['Version'] || '', arch: current['Architecture'] || '', publisher: current['Packager'] || '', date: current['Install Date'] || '', location: 'pacman' }); + } + cb(results); + }); + } + + // ---------- apk (Alpine) ---------- + function collectApk(cb) { + if (!binExists(['/sbin/apk', '/usr/bin/apk']) || (!dirExists('/lib/apk/db') && !dirExists('/var/lib/apk/db'))) { cb([]); return; } + + runSh('apk info -v 2>/dev/null', function (out) { + if (!out) { cb([]); return; } + var results = []; + out.split('\n').forEach(function (line) { + line = line.trim(); + if (!line) return; + var match = line.match(/^(.+)-(\d[^-]*)(-r\d+)?$/); + if (match) { + results.push({ name: match[1], version: match[2] + (match[3] || ''), arch: '', publisher: '', location: 'apk' }); + } else { + results.push({ name: line, version: '', arch: '', publisher: '', location: 'apk' }); + } + }); + cb(results); + }); + } + + // ---------- flatpak ---------- + function collectFlatpak(cb) { + if (!binExists(['/usr/bin/flatpak', '/bin/flatpak']) || !dirExists('/var/lib/flatpak')) { cb([]); return; } + + var results = []; + + runSh('flatpak list --system --columns=application,version,origin 2>/dev/null', function (sys) { + sys.split('\n').forEach(function (line) { + var p = line.split('\t'); + if (!p[0] || !p[0].trim()) return; + results.push({ name: p[0].trim(), version: (p[1] || '').trim(), arch: '', publisher: (p[2] || '').trim(), location: 'flatpak', scope: 'system' }); + }); + + runSh('flatpak list --user --columns=application,version,origin 2>/dev/null', function (usr) { + usr.split('\n').forEach(function (line) { + var p = line.split('\t'); + if (!p[0] || !p[0].trim()) return; + results.push({ name: p[0].trim(), version: (p[1] || '').trim(), arch: '', publisher: (p[2] || '').trim(), location: 'flatpak', scope: 'user' }); + }); + + cb(results); + }); + }); + } + + // ---------- snap ---------- + function collectSnap(cb) { + if (!binExists(['/usr/bin/snap', '/bin/snap']) || !dirExists('/var/lib/snapd/snaps')) { cb([]); return; } + + runSh('snap list --all 2>/dev/null', function (out) { + if (!out) { cb([]); return; } + var results = []; + out.split('\n').forEach(function (line) { + if (!line || line.indexOf('Name') === 0) return; + var p = line.trim().split(/\s+/); + if (p.length < 5 || !p[0]) return; + results.push({ name: p[0], version: p[1], arch: '', publisher: p[4] || '', location: 'snap' }); + }); + cb(results); + }); + } + + // Run every collector concurrently (non-blocking), keep results in source order, + // and resolve once all have reported. Each collector calls its callback exactly + // once; the bucket guard makes a stray double-callback harmless. + var collectors = [collectDpkg, collectRpm, collectPacman, collectApk, collectFlatpak, collectSnap]; + var buckets = new Array(collectors.length); + var pending = collectors.length; + function onCollected(i, r) { + if (buckets[i] !== undefined) { return; } + buckets[i] = (r && r.length) ? r : []; + if (--pending === 0) { + try { ret._res([].concat.apply([], buckets)); } catch (e) { ret._rej(e); } + } + } + collectors.forEach(function (collector, i) { + try { collector(function (r) { onCollected(i, r); }); } + catch (e) { onCollected(i, []); } + }); + + return (ret); +} + +if (process.platform == 'linux') { + module.exports = { av: av, firewall: firewall, packages: packages }; +} +else +{ + var not_supported = function () { throw (process.platform + ' not supported'); }; + module.exports = { av: not_supported, firewall: not_supported, packages: not_supported }; +} \ No newline at end of file diff --git a/agents/modules_meshcore/mac-info.js b/agents/modules_meshcore/mac-info.js new file mode 100644 index 0000000000..2d042819eb --- /dev/null +++ b/agents/modules_meshcore/mac-info.js @@ -0,0 +1,92 @@ + +var promise = require('promise'); + +function apps() { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + + function runSh(script) { + try { + var child = require('child_process').execFile('/bin/sh', ['sh']); + child.stdout.str = ''; + child.stdout.on('data', function (d) { child.stdout.str += d; }); + child.stdin.write(script + '\nexit\n'); + child.waitExit(); + return child.stdout.str.trim(); + } catch (ex) { return ''; } + } + + // Single shell process: PlistBuddy handles both XML and binary plists. + // Outputs tab-separated: name, version, publisher, date, scope — one app per line. + var script = [ + 'PB=/usr/libexec/PlistBuddy', + 'process_app() {', + ' app="$1" scope="$2"', + ' plist="$app/Contents/Info.plist"', + ' [ -f "$plist" ] || return', + ' name=$(basename "$app" .app)', + ' version=$("$PB" -c "Print :CFBundleShortVersionString" "$plist" 2>/dev/null)', + ' publisher=$("$PB" -c "Print :NSHumanReadableCopyright" "$plist" 2>/dev/null)', + ' if [ -z "$publisher" ]; then', + ' bid=$("$PB" -c "Print :CFBundleIdentifier" "$plist" 2>/dev/null)', + ' publisher=$(echo "$bid" | awk -F. \'{if(NF>=2){s=$2; print toupper(substr(s,1,1)) substr(s,2)}}\')', + ' fi', + ' date=$(stat -f "%Sm" -t "%Y-%m-%d" "$app" 2>/dev/null)', + ' arch=""', + ' exe=$("$PB" -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)', + ' if [ -n "$exe" ]; then', + ' lipoout=$(lipo -info "$app/Contents/MacOS/$exe" 2>/dev/null)', + ' if echo "$lipoout" | grep -q "x86_64" && echo "$lipoout" | grep -q "arm64"; then', + ' arch="universal"', + ' elif echo "$lipoout" | grep -q "arm64"; then', + ' arch="arm64"', + ' elif echo "$lipoout" | grep -q "x86_64"; then', + ' arch="x86_64"', + ' fi', + ' fi', + ' printf "%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n" \\', + ' "$(printf "%s" "$name" | tr "\\t\\n" " ")" \\', + ' "$(printf "%s" "$version" | tr "\\t\\n" " ")" \\', + ' "$(printf "%s" "$publisher" | tr "\\t\\n" " ")" \\', + ' "$date" "$arch" "$scope"', + '}', + 'for sysdir in /Applications /System/Applications /System/Applications/Utilities; do', + ' [ -d "$sysdir" ] || continue', + ' find "$sysdir" -maxdepth 1 -name "*.app" -type d 2>/dev/null | while IFS= read -r app; do', + ' process_app "$app" system', + ' done', + 'done', + 'for userdir in /Users/*/; do', + ' user=$(basename "$userdir")', + ' [ "$user" = Shared ] && continue', + ' case "$user" in .*) continue;; esac', + ' appsdir="${userdir}Applications"', + ' [ -d "$appsdir" ] || continue', + ' find "$appsdir" -maxdepth 1 -name "*.app" -type d 2>/dev/null | while IFS= read -r app; do', + ' process_app "$app" user', + ' done', + 'done' + ].join('\n'); + + try { + var out = runSh(script); + var results = []; + if (out) { + out.split('\n').forEach(function (line) { + var p = line.split('\t'); + if (p.length < 6 || !p[0]) return; + results.push({ name: p[0], version: p[1] || '', publisher: p[2] || '', date: p[3] || '', arch: p[4] || '', location: p[5] || 'system' }); + }); + } + ret._res(results); + } catch (e) { + ret._rej(e); + } + return (ret); +} + +if (process.platform == 'darwin') { + module.exports = { apps: apps }; +} else { + var not_supported = function () { throw (process.platform + ' not supported'); }; + module.exports = { apps: not_supported }; +} diff --git a/agents/modules_meshcore/sysinfo.js b/agents/modules_meshcore/sysinfo.js index cc13574dde..014d629b46 100644 --- a/agents/modules_meshcore/sysinfo.js +++ b/agents/modules_meshcore/sysinfo.js @@ -230,7 +230,7 @@ function windows_thermals() { var ret = []; try { - ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']); + ret = require('win-wmi-fixed').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']); if (ret[0]) { for (var i = 0; i < ret.length; ++i) { ret[i]['CurrentTemperature'] = ((parseFloat(ret[i]['CurrentTemperature']) / 10) - 273.15).toFixed(2); diff --git a/agents/modules_meshcore/win-info.js b/agents/modules_meshcore/win-info.js index 69377cc981..4247a725c5 100644 --- a/agents/modules_meshcore/win-info.js +++ b/agents/modules_meshcore/win-info.js @@ -16,10 +16,13 @@ limitations under the License. var promise = require('promise'); +// We use the environment variable directly or the standard Windows path +var psPath = (process.env['SystemRoot'] ? process.env['SystemRoot'] : 'C:\\Windows') + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; + function qfe() { try { - var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_QuickFixEngineering'); + var tokens = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT * FROM Win32_QuickFixEngineering'); if (tokens[0]){ for (var index = 0; index < tokens.length; index++) { for (var key in tokens[index]) { @@ -179,83 +182,123 @@ function pendingReboot() return (ret); } -function installedApps() -{ - var promise = require('promise'); - var ret = new promise(function (a, r) { this._resolve = a; this._reject = r; }); - - var code = "\ - var reg = require('win-registry');\ - var result = [];\ - var val, tmp;\ - var items = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall');\ - for (var key in items.subkeys)\ - {\ - val = {};\ - try\ - {\ - val.name = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'DisplayName');\ - }\ - catch(e)\ - {\ - continue;\ - }\ - try\ - {\ - val.version = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'DisplayVersion');\ - if (val.version == '') { delete val.version; }\ - }\ - catch(e)\ - {\ - }\ - try\ - {\ - val.location = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'InstallLocation');\ - if (val.location == '') { delete val.location; }\ - }\ - catch(e)\ - {\ - }\ - try\ - {\ - val.installdate = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'InstallDate');\ - if (val.installdate == '') { delete val.installdate; }\ - }\ - catch(e)\ - {\ - }\ - result.push(val);\ - }\ - console.log(JSON.stringify(result,'', 1));process.exit();"; - - ret.child = require('child_process').execFile(process.execPath, [process.execPath.split('\\').pop().split('.exe')[0], '-exec "' + code + '"']); - ret.child.promise = ret; - ret.child.stdout.str = ''; ret.child.stdout.on('data', function (c) { this.str += c.toString(); }); - ret.child.on('exit', function (c) { this.promise._resolve(JSON.parse(this.stdout.str.trim())); }); +function installedApps() { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + var registry = require('win-registry'); + var HKEY = registry.HKEY; + var results = []; + var registryPaths = [ + 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall', + 'SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall' + ]; + for (var i in registryPaths) { + try { + var path = registryPaths[i]; + var keyInfo = registry.QueryKey(HKEY.LocalMachine, path); + if (!keyInfo || !keyInfo.subkeys) continue; + for (var j = 0; j < keyInfo.subkeys.length; j++) { + var subPath = path + '\\' + keyInfo.subkeys[j]; + var name = regQuery(HKEY.LocalMachine, subPath, 'DisplayName'); + if (name && name != '') { + results.push({ + name: name, + version: regQuery(HKEY.LocalMachine, subPath, 'DisplayVersion') || '', + publisher: regQuery(HKEY.LocalMachine, subPath, 'Publisher') || '', + uninstall: regQuery(HKEY.LocalMachine, subPath, 'QuietUninstallString') || regQuery(HKEY.LocalMachine, subPath, 'UninstallString') || '', + location: regQuery(HKEY.LocalMachine, subPath, 'InstallLocation') || '', + date: regQuery(HKEY.LocalMachine, subPath, 'InstallDate') || '' + }); + } + } + } catch (e) { } + } + ret._res(results); return (ret); } -function installedStoreApps(){ +function installedStoreApps() { + var ret = new promise(function (a, r) { this._resolve = a; this._reject = r; }); + + // Basierend auf deiner funktionierenden Version + Scope-Erkennung + var psCommand = [ + "$ErrorActionPreference = 'SilentlyContinue'", + "$allUsersApps = @(Get-AppxPackage -AllUsers)", + "$allUsersPkgNames = @($allUsersApps | Select-Object -ExpandProperty PackageFullName)", + "$userOnlyApps = @(Get-AppxPackage | Where-Object { $allUsersPkgNames -notcontains $_.PackageFullName })", + "$provPkgs = @(Get-AppxProvisionedPackage -Online | Select-Object -ExpandProperty DisplayName)", + "$results = @()", + "foreach ($app in $allUsersApps) {", + " if ($app.Name -and $app.Name -notlike 'Microsoft.Windows.*' -and $app.Name -notlike 'windows.*' -and $app.Name -notlike '*_neutral_*') {", + " $scope = 'System'", + " if ($provPkgs -contains $app.Name) { $scope = 'System+Prov' }", + " $results += [PSCustomObject]@{ Name=$app.Name; Version=[string]$app.Version; PackageFullName=$app.PackageFullName; Publisher=$app.Publisher; Scope=$scope }", + " }", + "}", + "foreach ($app in $userOnlyApps) {", + " if ($app.Name -and $app.Name -notlike 'Microsoft.Windows.*' -and $app.Name -notlike 'windows.*' -and $app.Name -notlike '*_neutral_*') {", + " $scope = 'User'", + " if ($provPkgs -contains $app.Name) { $scope = 'User+Prov' }", + " $results += [PSCustomObject]@{ Name=$app.Name; Version=[string]$app.Version; PackageFullName=$app.PackageFullName; Publisher=$app.Publisher; Scope=$scope }", + " }", + "}", + "$results | Sort-Object Name -Unique | ConvertTo-Json -Compress" + ].join("; "); + try { - var tokens = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT * FROM Win32_InstalledStoreProgram'); - if (tokens[0]){ - for (var index = 0; index < tokens.length; index++) { - for (var key in tokens[index]) { - if (key.startsWith('__')) delete tokens[index][key]; + var psPath = (process.env['SystemRoot'] || 'C:\\Windows') + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; + + ret.child = require('child_process').execFile( + psPath, + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', psCommand], + { timeout: 60000, maxBuffer: 10 * 1024 * 1024 } + ); + + ret.child.promise = ret; + ret.child.stdout.str = ''; + ret.child.stdout.on('data', function (chunk) { this.str += chunk.toString(); }); + + ret.child.on('exit', function (code) { + try { + var output = this.stdout.str.trim(); + + if (output === '' || output === 'null' || output === '[]') { + this.promise._resolve([]); + return; } + + var data = JSON.parse(output); + if (!Array.isArray(data)) { data = [data]; } + + var apps = data.map(function(app) { + return { + name: app.Name || '', + version: app.Version || '', + publisher: app.Publisher || '', + packageFullName: app.PackageFullName || '', + scope: app.Scope || '', + uninstall: 'Remove-AppxPackage -Package "' + (app.PackageFullName || '') + '" -AllUsers' + }; + }); + + this.promise._resolve(apps); + } catch (e) { + this.promise._resolve([]); } - return (tokens); - } else { - return ([]); - }; + }); + + ret.child.on('error', function (err) { + this.promise._resolve([]); + }); } catch (ex) { - return ([]); + ret._resolve([]); } + + return (ret); } function defender(){ try { - var tokens = require('win-wmi').query('ROOT\\Microsoft\\Windows\\Defender', 'SELECT * FROM MSFT_MpComputerStatus', ['RealTimeProtectionEnabled','IsTamperProtected','AntivirusSignatureVersion','AntivirusSignatureLastUpdated']); + var tokens = require('win-wmi-fixed').query('ROOT\\Microsoft\\Windows\\Defender', 'SELECT * FROM MSFT_MpComputerStatus', ['RealTimeProtectionEnabled','IsTamperProtected','AntivirusSignatureVersion','AntivirusSignatureLastUpdated']); if (tokens[0]){ var info = { RealTimeProtection: tokens[0].RealTimeProtectionEnabled, TamperProtected: tokens[0].IsTamperProtected }; if (tokens[0].AntivirusSignatureVersion) { info.AntivirusSignatureVersion = tokens[0].AntivirusSignatureVersion; } diff --git a/agents/modules_meshcore/win-updates.js b/agents/modules_meshcore/win-updates.js new file mode 100644 index 0000000000..1fb1882fb8 --- /dev/null +++ b/agents/modules_meshcore/win-updates.js @@ -0,0 +1,474 @@ +// Windows Update available updates lister via Windows Update Agent COM API. +// Uses IUpdateSession -> IUpdateSearcher (async) to find updates not yet installed. + +var promise = require('promise'); + +var CLSID_UpdateSession = '{4CB43D7F-7EEE-4906-8698-60DA1C38F2FE}'; +// IID observed from QueryInterface wire call for IUpdateSearchCompletedCallback +var IID_IUSCC = '58E0AE88B0D42547A2F1814A67AE964C'; +var E_NOINTERFACE = 0x80004002; + +var _GM = require('_GenericMarshal'); +var OleAut32 = _GM.CreateNativeProxy('OleAut32.dll'); +OleAut32.CreateMethod('SysAllocString'); +OleAut32.CreateMethod('SysFreeString'); + +function makeBSTR(str) { + return OleAut32.SysAllocString(_GM.CreateVariable(str, { wide: true })); +} + +var UpdateSessionFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_ClientApplicationID', 'put_ClientApplicationID', 'get_ReadOnly', + 'get_WebProxy', 'put_WebProxy', + 'CreateUpdateSearcher', 'CreateUpdateDownloader', 'CreateUpdateInstaller' +]; + +var UpdateSearcherFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_CanAutomaticallyUpgradeService', 'put_CanAutomaticallyUpgradeService', + 'get_ClientApplicationID', 'put_ClientApplicationID', + 'get_IncludePotentiallySupersededUpdates', 'put_IncludePotentiallySupersededUpdates', + 'get_ServerSelection', 'put_ServerSelection', + 'BeginSearch', 'EndSearch', 'EscapeString', 'QueryHistory', 'Search', + 'get_Online', 'put_Online', 'GetTotalHistoryCount', 'get_ServiceID', 'put_ServiceID' +]; + +var SearchResultFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_ResultCode', 'get_RootCategories', 'get_Updates', 'get_Warnings' +]; + +var UpdateCollectionFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_Item', 'put_Item', 'get__NewEnum', 'get_Count', 'get_ReadOnly', + 'Add', 'Clear', 'Copy', 'Insert', 'RemoveAt' +]; + +var UpdateFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_Title', // 7 + 'get_AutoSelectOnWebSites', // 8 + 'get_BundledUpdates', // 9 + 'get_CanRequireSource', // 10 + 'get_Categories', // 11 + 'get_Deadline', // 12 + 'get_DeltaCompressedContentAvailable', // 13 + 'get_DeltaCompressedContentPreferred', // 14 + 'get_Description', // 15 + 'get_EulaAccepted', // 16 + 'get_EulaText', // 17 + 'get_HandlerID', // 18 + 'get_Identity', // 19 + 'get_Image', // 20 + 'get_InstallationBehavior', // 21 + 'get_IsBeta', // 22 + 'get_IsDownloaded', // 23 + 'get_IsHidden', // 24 + 'put_IsHidden', // 25 + 'get_IsInstalled', // 26 + 'get_IsMandatory', // 27 + 'get_IsUninstallable', // 28 + 'get_Languages', // 29 + 'get_LastDeploymentChangeTime', // 30 + 'get_MaxDownloadSize', // 31 + 'get_MinDownloadSize', // 32 + 'get_MoreInfoUrls', // 33 + 'get_MsrcSeverity', // 34 + 'get_RecommendedCpuSpeed', // 35 + 'get_RecommendedHardDiskSpace', // 36 + 'get_RecommendedMemory', // 37 + 'get_ReleaseNotes', // 38 + 'get_SecurityBulletinIDs', // 39 + 'get_SupersededUpdateIDs', // 40 + 'get_SupportUrl', // 41 + 'get_Type', // 42 + 'get_UninstallationNotes', // 43 + 'get_UninstallationBehavior', // 44 + 'get_UninstallationSteps', // 45 + 'AcceptEula', // 46 + 'get_KBArticleIDs', // 47 + 'get_DeploymentAction', // 48 + 'CopyFromCache', // 49 + 'get_DownloadPriority', // 50 + 'get_DownloadContents' // 51 +]; + +var UpdateIdentityFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_RevisionNumber', 'get_UpdateID' +]; + +// IUpdateHistoryEntryCollection +var UpdateHistoryCollectionFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_Item', // 7 + 'get__NewEnum', // 8 + 'get_Count' // 9 +]; + +// IUpdateHistoryEntry +var UpdateHistoryEntryFunctions = [ + 'QueryInterface', 'AddRef', 'Release', + 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke', + 'get_Operation', // 7 + 'get_ResultCode', // 8 + 'get_Date', // 9 + 'get_UpdateIdentity', // 10 + 'get_HResult', // 11 (extra method between identity and title) + 'get_Title', // 12 + 'get_Description', // 13 + 'get_UnmappedResultCode', // 14 + 'get_ClientApplicationID', // 15 + 'get_ServerSelection', // 16 + 'get_ServiceID', // 17 + 'get_UninstallationSteps', // 18 + 'get_UninstallationNotes', // 19 + 'get_SupportUrl' // 20 +]; + +var active_handlers = {}; +var _cx_counter = 30; + + +function releaseCOM(obj) { + if (obj && obj.funcs) { try { obj.funcs.Release(obj); } catch (e) {} } +} + +function readBSTR(ptr) { + try { return ptr.Deref().Wide2UTF8; } catch (e) { return ''; } +} + +function readVarBool(v) { + try { return v.toBuffer().readInt16LE() !== 0; } catch (e) { return false; } +} + +function readUpdates(searchResultPtr, COM, GM) { + var result = []; + var searchResult = searchResultPtr.Deref(); + searchResult.funcs = COM.marshalFunctions(searchResult, SearchResultFunctions); + + var rcV = GM.CreateVariable(4); + var resultCode = searchResult.funcs.get_ResultCode(searchResult, rcV).Val === 0 ? rcV.toBuffer().readInt32LE() : -1; + if (resultCode !== 2 && resultCode !== 3) { releaseCOM(searchResult); return []; } + + var updatesPtr = GM.CreatePointer(); + if (searchResult.funcs.get_Updates(searchResult, updatesPtr).Val !== 0) { releaseCOM(searchResult); return []; } + + var updates = updatesPtr.Deref(); + updates.funcs = COM.marshalFunctions(updates, UpdateCollectionFunctions); + + var countV = GM.CreateVariable(4); + updates.funcs.get_Count(updates, countV); + var count = countV.toBuffer().readInt32LE(); + + for (var i = 0; i < count; i++) { + var updatePtr = GM.CreatePointer(); + if (updates.funcs.get_Item(updates, i, updatePtr).Val !== 0) continue; + + var update = updatePtr.Deref(); + update.funcs = COM.marshalFunctions(update, UpdateFunctions); + + var entry = {}; + try { + var titlePtr = GM.CreatePointer(); + entry.title = update.funcs.get_Title(update, titlePtr).Val === 0 ? readBSTR(titlePtr) : ''; + + var sevPtr = GM.CreatePointer(); + entry.severity = update.funcs.get_MsrcSeverity(update, sevPtr).Val === 0 ? readBSTR(sevPtr) : ''; + + var dlV = GM.CreateVariable(2); + entry.isDownloaded = update.funcs.get_IsDownloaded(update, dlV).Val === 0 ? readVarBool(dlV) : false; + + var mandV = GM.CreateVariable(2); + entry.isMandatory = update.funcs.get_IsMandatory(update, mandV).Val === 0 ? readVarBool(mandV) : false; + + var identPtr = GM.CreatePointer(); + entry.updateID = ''; + if (update.funcs.get_Identity(update, identPtr).Val === 0) { + var identFuncs = COM.marshalFunctions(identPtr.Deref(), UpdateIdentityFunctions); + var uidPtr = GM.CreatePointer(); + if (identFuncs.get_UpdateID(identPtr.Deref(), uidPtr).Val === 0) { entry.updateID = readBSTR(uidPtr); } + } + + // KB article IDs extracted from title e.g. "(KB1234567)" + entry.kbArticleIDs = []; + var kbMatches = entry.title.match(/KB\d+/gi); + if (kbMatches) { + for (var k = 0; k < kbMatches.length; k++) { + entry.kbArticleIDs.push(kbMatches[k].toUpperCase()); + } + } + } catch (e) {} + + releaseCOM(update); + result.push(entry); + } + + releaseCOM(updates); + releaseCOM(searchResult); + return result; +} + +function makeSearchCallback(GM, COM, p, searcher) { + var cx = _cx_counter; + _cx_counter += 4; + var handler = COM.marshalInterface([ + { + cx: cx, parms: 3, name: 'QueryInterface', func: function (j, riid, ppv) + { + var hex = riid.Deref(0, 16).toBuffer().toString('hex').toUpperCase(); + var ret = GM.CreateVariable(4); + if (hex === '0000000000000000C000000000000046' || hex === IID_IUSCC) { + j.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer()); + ret.increment(++this.refcount, true); + } else { + ret.increment(E_NOINTERFACE, true); + } + return ret; + } + }, + { cx: cx+1, parms: 1, name: 'AddRef', func: function () { return GM.CreateVariable(4).increment(++this.refcount, true); } }, + { cx: cx+2, parms: 1, name: 'Release', func: function () { + if (--this.refcount === 0) { cleanup(this); } + return GM.CreateVariable(4).increment(this.refcount >>> 0, true); + }}, + { + cx: cx+3, parms: 3, name: 'Invoke', func: function (j, searchJob, callbackArgs) + { + var searchResultPtr = GM.CreatePointer(); + var hr = this.searcher.funcs.EndSearch(this.searcher, searchJob, searchResultPtr).Val; + if (hr !== 0) { + this.p._rej(new Error('EndSearch failed: 0x' + (hr >>> 0).toString(16))); + } else { + try { this.p._res(readUpdates(searchResultPtr, COM, GM)); } + catch (e) { this.p._rej(e); } + } + var self = this; + setImmediate(function () { if (--self.refcount === 0) { cleanup(self); } }); + return GM.CreateVariable(4).increment(0, true); + } + } + ]); + + handler.refcount = 1; + handler.p = p; + handler.searcher = searcher; + active_handlers[handler._hashCode()] = handler; + return handler; +} + +function cleanup(h) { + if (h.cleanup) { h.cleanup(); } + delete active_handlers[h._hashCode()]; +} + +function getAvailableUpdates() { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + + try { + var GM = require('_GenericMarshal'); + var COM = require('win-com'); + + var session = COM.createInstance(COM.CLSIDFromString(CLSID_UpdateSession), COM.IID_IUnknown); + session.funcs = COM.marshalFunctions(session, UpdateSessionFunctions); + + var searcherPtr = GM.CreatePointer(); + var hr = session.funcs.CreateUpdateSearcher(session, searcherPtr).Val; + if (hr !== 0) { throw new Error('CreateUpdateSearcher failed: 0x' + (hr >>> 0).toString(16)); } + + var searcher = searcherPtr.Deref(); + searcher.funcs = COM.marshalFunctions(searcher, UpdateSearcherFunctions); + + var criteriaBSTR = makeBSTR('IsInstalled=0 and IsHidden=0'); + var searchJobPtr = GM.CreatePointer(); + var stateV = GM.CreateVariable(16); // VT_EMPTY VARIANT + var callback = makeSearchCallback(GM, COM, ret, searcher); + + hr = searcher.funcs.BeginSearch(searcher, criteriaBSTR, callback, stateV, searchJobPtr).Val; + OleAut32.SysFreeString(criteriaBSTR); + + if (hr !== 0) { + cleanup(callback); + releaseCOM(searcher); + releaseCOM(session); + throw new Error('BeginSearch failed: 0x' + (hr >>> 0).toString(16)); + } + + } catch (ex) { + ret._rej(ex); + } + + return ret; +} + +function getInstalledUpdates() { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + + try { + var GM = require('_GenericMarshal'); + var COM = require('win-com'); + + var session = COM.createInstance(COM.CLSIDFromString(CLSID_UpdateSession), COM.IID_IUnknown); + session.funcs = COM.marshalFunctions(session, UpdateSessionFunctions); + + var searcherPtr = GM.CreatePointer(); + var hr = session.funcs.CreateUpdateSearcher(session, searcherPtr).Val; + if (hr !== 0) { throw new Error('CreateUpdateSearcher failed: 0x' + (hr >>> 0).toString(16)); } + + var searcher = searcherPtr.Deref(); + searcher.funcs = COM.marshalFunctions(searcher, UpdateSearcherFunctions); + + var criteriaBSTR = makeBSTR('IsInstalled=1'); + var searchJobPtr = GM.CreatePointer(); + var stateV = GM.CreateVariable(16); + var callback = makeSearchCallback(GM, COM, ret, searcher); + + hr = searcher.funcs.BeginSearch(searcher, criteriaBSTR, callback, stateV, searchJobPtr).Val; + OleAut32.SysFreeString(criteriaBSTR); + + if (hr !== 0) { + cleanup(callback); + releaseCOM(searcher); + releaseCOM(session); + throw new Error('BeginSearch failed: 0x' + (hr >>> 0).toString(16)); + } + + } catch (ex) { + ret._rej(ex); + } + + return ret; +} + +function getInstalledUpdateHistory() { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + + var GM, COM, session, searcher, history; + try { + GM = require('_GenericMarshal'); + COM = require('win-com'); + + session = COM.createInstance(COM.CLSIDFromString(CLSID_UpdateSession), COM.IID_IUnknown); + session.funcs = COM.marshalFunctions(session, UpdateSessionFunctions); + + var searcherPtr = GM.CreatePointer(); + var hr = session.funcs.CreateUpdateSearcher(session, searcherPtr).Val; + if (hr !== 0) { throw new Error('CreateUpdateSearcher failed: 0x' + (hr >>> 0).toString(16)); } + + searcher = searcherPtr.Deref(); + searcher.funcs = COM.marshalFunctions(searcher, UpdateSearcherFunctions); + + var totalV = GM.CreateVariable(4); + hr = searcher.funcs.GetTotalHistoryCount(searcher, totalV).Val; + if (hr !== 0) { throw new Error('GetTotalHistoryCount failed: 0x' + (hr >>> 0).toString(16)); } + var total = totalV.toBuffer().readInt32LE(); + + var historyPtr = GM.CreatePointer(); + hr = searcher.funcs.QueryHistory(searcher, 0, total, historyPtr).Val; + if (hr !== 0) { throw new Error('QueryHistory failed: 0x' + (hr >>> 0).toString(16)); } + + history = historyPtr.Deref(); + history.funcs = COM.marshalFunctions(history, UpdateHistoryCollectionFunctions); + + var countV = GM.CreateVariable(4); + history.funcs.get_Count(history, countV); + var count = countV.toBuffer().readInt32LE(); + + var result = []; + var i = 0; + var BATCH = 100; + + function processBatch() { + try { + var end = Math.min(i + BATCH, count); + while (i < end) { + var entryPtr2 = GM.CreatePointer(); + if (history.funcs.get_Item(history, i, entryPtr2).Val === 0) { + var ec = entryPtr2.Deref(); + ec.funcs = COM.marshalFunctions(ec, UpdateHistoryEntryFunctions); + try { + var opV = GM.CreateVariable(4); + var op = ec.funcs.get_Operation(ec, opV).Val === 0 ? opV.toBuffer().readInt32LE() : -1; + var rcV = GM.CreateVariable(4); + var rc = ec.funcs.get_ResultCode(ec, rcV).Val === 0 ? rcV.toBuffer().readInt32LE() : -1; + if (op === 1 && rc === 2) { + var entry = {}; + var tPtr = GM.CreatePointer(); + entry.title = ec.funcs.get_Title(ec, tPtr).Val === 0 ? readBSTR(tPtr) : ''; + entry.kbArticleIDs = []; + var kbM = entry.title.match(/KB\d+/gi); + if (kbM) { for (var k = 0; k < kbM.length; k++) { entry.kbArticleIDs.push(kbM[k].toUpperCase()); } } + result.push(entry); + } + } catch (e) { } + releaseCOM(ec); + } + i++; + } + if (i < count) { + ret._batchTimer = setTimeout(processBatch, 0); + } else { + releaseCOM(history); + releaseCOM(searcher); + releaseCOM(session); + ret._res(result); + } + } catch (ex) { + releaseCOM(history); + releaseCOM(searcher); + releaseCOM(session); + ret._rej(ex); + } + } + processBatch(); + } catch (ex) { + if (history) { releaseCOM(history); } + if (searcher) { releaseCOM(searcher); } + if (session) { releaseCOM(session); } + ret._rej(ex); + } + return ret; +} + +function getInstalledUpdatesDeDuplicated() { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + getInstalledUpdateHistory().then(function (all) { + // Deduplicate by KB number, keeping most recent (history is newest-first so first seen wins) + // For entries with no KB number, deduplicate by full title + var seen = {}; + var result = []; + for (var i = 0; i < all.length; i++) { + var key = all[i].kbArticleIDs.length > 0 ? all[i].kbArticleIDs.join(',') : all[i].title; + if (!seen[key]) { + seen[key] = true; + result.push(all[i]); + } + } + ret._res(result); + }).catch(function (e) { ret._rej(e); }); + return ret; +} + +if (process.platform === 'win32') { + module.exports = { getAvailableUpdates: getAvailableUpdates, getInstalledUpdates: getInstalledUpdates, getInstalledUpdateHistory: getInstalledUpdateHistory, getInstalledUpdatesDeDuplicated: getInstalledUpdatesDeDuplicated }; +} else { + var _notSupported = function () { + var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + ret._rej(new Error(process.platform + ' not supported')); + return ret; + }; + module.exports = { + getAvailableUpdates: _notSupported, + getInstalledUpdates: _notSupported, + getInstalledUpdateHistory: _notSupported, + getInstalledUpdatesDeDuplicated: _notSupported + }; +} diff --git a/agents/modules_meshcore/win-volumes.js b/agents/modules_meshcore/win-volumes.js index 59e87c1a2e..22f903767b 100644 --- a/agents/modules_meshcore/win-volumes.js +++ b/agents/modules_meshcore/win-volumes.js @@ -30,7 +30,7 @@ function trimObject(j) function getVolumes() { - var v = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_Volume'); + var v = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT * FROM Win32_Volume'); var i; var ret = {}; @@ -40,7 +40,7 @@ function getVolumes() ret[v[i].DeviceID] = trimObject(v[i]); } try { - v = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume'); + v = require('win-wmi-fixed').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume'); for (i in v) { var tmp = trimObject(v[i]); @@ -55,74 +55,156 @@ function getVolumes() function windows_volumes() { + var drives = {}; + var error = null; var promise = require('promise'); var p1 = new promise(function (res, rej) { this._res = res; this._rej = rej; }); - var ret = {}; - var values = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_LogicalDisk', ['DeviceID', 'VolumeName', 'FileSystem', 'Size', 'FreeSpace', 'DriveType']); - if(values[0]){ - for (var i = 0; i < values.length; ++i) { - var drive = values[i]['DeviceID'].slice(0,-1); - ret[drive] = { - name: (values[i]['VolumeName'] ? values[i]['VolumeName'] : ""), - type: (values[i]['FileSystem'] ? values[i]['FileSystem'] : "Unknown"), - size: (values[i]['Size'] ? values[i]['Size'] : 0), - sizeremaining: (values[i]['FreeSpace'] ? values[i]['FreeSpace'] : 0), - removable: (values[i]['DriveType'] == 2), - cdrom: (values[i]['DriveType'] == 5) - }; + // RegExps for the specific patterns in the manage-bde output, case-insensitive multiline + var reID = new RegExp("{[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}}", "mi"); + var rePass = new RegExp("[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}", "mi"); + + var pending = 0, done = false; + function resolver() { if (done && (pending === 0)) { p1._res({ error: error, drives: drives }); } } + + try { + var wmi = require('win-wmi-fixed'); + + wmi.query('ROOT\\CIMV2', 'SELECT DeviceID,VolumeName,FileSystem,Size,FreeSpace,DriveType FROM Win32_LogicalDisk', ['DeviceID', 'VolumeName', 'FileSystem', 'Size', 'FreeSpace', 'DriveType'], false, 5000) + .forEach(function (disk) { + if (!disk || !disk['DeviceID']) { return; } // skip rows without a DeviceID. Shouldn't be possible, but could in case of wmi funkyness + var drive = disk['DeviceID'].slice(0, -1); + drives[drive] = { + name: disk['VolumeName'] || '', + type: disk['FileSystem'] || 'Unknown', + size: parseInt(disk['Size']) || 0, + sizeremaining: parseInt(disk['FreeSpace']) || 0, + dType: disk['DriveType'] || 0 + }; + }); + + // The MicrosoftVolumeEncryption namespace is admin-only and manage-bde needs elevation; skip both entirely when not elevated, saves waiting unneccessary on time-out of wmi-query if run as user + if (require('user-sessions').isRoot()) { + var child_process = require('child_process'); + wmi.query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT DriveLetter,ConversionStatus,ProtectionStatus,EncryptionMethod FROM Win32_EncryptableVolume', ['DriveLetter', 'ConversionStatus', 'ProtectionStatus', 'EncryptionMethod'], false, 5000) + .forEach(function (vol) { + if (!vol || !vol['DriveLetter']) { return; } // there can be volumes without a DriveLetter(=null), which errors the slice + var drive = vol['DriveLetter'].slice(0, -1); + if (!drives[drive]) { return; } // no matching logical disk, skip for now. + drives[drive].volumeStatus = vol['ConversionStatus']; + drives[drive].encryptionMethod = vol['EncryptionMethod']; + drives[drive].protectionStatus = vol['ProtectionStatus']; + // Only run manage-bde on encrypted drives (ConversionStatus/volumeStatus 0 = FullyDecrypted, otherwise some sort of encryption) + if (drives[drive].volumeStatus != 0) { + pending++; + var keychild = child_process.execFile(process.env['windir'] + '\\system32\\manage-bde.exe', ['manage-bde', '-protectors', '-get', drive + ':', '-Type', 'recoverypassword'], {}); + keychild.stdout.str = ''; + keychild.stdout.on('data', function (c) { this.str += c.toString(); }); + keychild.target = drive; + // Guard against a hung manage-bde + keychild.timeout = setTimeout(function () { try { keychild.kill(); } catch (ex) { } }, 4000); + keychild.on('exit', function () { + clearTimeout(this.timeout); + var id = this.stdout.str.match(reID); + var rp = this.stdout.str.match(rePass); + // a recovery password protector should always have an identifier + if (id) { drives[this.target].identifier = id[0]; } + // recoveryPW can be empty if volume is locked + if (rp) { drives[this.target].recoveryPassword = rp[0]; } + pending--; + resolver(); + }); + } + }); } + } catch (e) { + console.log('windows_volumes error: ' + (e && e.message ? e.message : e)); + error = e; // add error, fall through to resolve below } + done = true; + resolver(); // if not root, resolve with partial info, otherwise the last child process resolves + return (p1); +} + +// Same as windows_volumes() but get the BitLocker recovery password through wmi. And win7 added +function windows_volumes_wmi() +{ + var promise = require('promise'); + var wmi = require('win-wmi-fixed'); + const NS_VE = 'ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption'; + var p1 = new promise(function (res, rej) { this._res = res; this._rej = rej; }); + var drives = {}; + try { - values = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume', ['DriveLetter','ConversionStatus','ProtectionStatus']); - if(values[0]){ - for (var i = 0; i < values.length; ++i) { - var drive = values[i]['DriveLetter'].slice(0,-1); - var statuses = { - 0: 'FullyDecrypted', - 1: 'FullyEncrypted', - 2: 'EncryptionInProgress', - 3: 'DecryptionInProgress', - 4: 'EncryptionPaused', - 5: 'DecryptionPaused' + // use win32_volume, for future property expansion and link through DeviceID + wmi.query('ROOT\\CIMV2', 'SELECT DriveLetter,Label,FileSystem,Capacity,FreeSpace,DriveType FROM Win32_Volume', ['DriveLetter', 'Label', 'FileSystem', 'Capacity', 'FreeSpace', 'DriveType'], false, 5000) + .forEach(function (vol) { + if (!vol || vol['DriveLetter'] == null) { return; } // skip letterless volumes for now + var drive = vol['DriveLetter'].slice(0, -1); + drives[drive] = { + name: vol['Label'] || '', + type: vol['FileSystem'] || 'Unknown', + size: parseInt(vol['Capacity']) || 0, + sizeremaining: parseInt(vol['FreeSpace']) || 0, + dType: vol['DriveType'] || 0 }; - ret[drive].volumeStatus = statuses.hasOwnProperty(values[i].ConversionStatus) ? statuses[values[i].ConversionStatus] : 'FullyDecrypted'; - ret[drive].protectionStatus = (values[i].ProtectionStatus == 0 ? 'Off' : (values[i].ProtectionStatus == 1 ? 'On' : 'Unknown')); - try { - var foundIDMarkedLine = false, foundMarkedLine = false, identifier = '', password = ''; - var keychild = require('child_process').execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'manage-bde -protectors -get ' + drive + ': -Type recoverypassword'], {}); - keychild.stdout.str = ''; keychild.stdout.on('data', function (c) { this.str += c.toString(); }); - keychild.waitExit(); - var lines = keychild.stdout.str.trim().split('\r\n'); - for (var x = 0; x < lines.length; x++) { // Loop each line - var abc = lines[x].trim(); - var englishidpass = (abc !== '' && abc.includes('Numerical Password:')); // English ID - var germanidpass = (abc !== '' && abc.includes('Numerisches Kennwort:')); // German ID - var frenchidpass = (abc !== '' && abc.includes('Mot de passe num')); // French ID - var englishpass = (abc !== '' && abc.includes('Password:') && !abc.includes('Numerical Password:')); // English Password - var germanpass = (abc !== '' && abc.includes('Kennwort:') && !abc.includes('Numerisches Kennwort:')); // German Password - var frenchpass = (abc !== '' && abc.includes('Mot de passe :') && !abc.includes('Mot de passe num')); // French Password - if (englishidpass || germanidpass || frenchidpass|| englishpass || germanpass || frenchpass) { - var nextline = lines[x + 1].trim(); - if (x + 1 < lines.length && (nextline !== '' && (nextline.startsWith('ID:') || nextline.startsWith('ID :')) )) { - identifier = nextline.replace('ID:','').replace('ID :', '').trim(); - foundIDMarkedLine = true; - }else if (x + 1 < lines.length && nextline !== '') { - password = nextline; - foundMarkedLine = true; + }); + + // The MicrosoftVolumeEncryption namespace is admin-only and manage-bde needs elevation; skip entirely when not elevated, saves waiting on the wmi-query time-out if run as user + if (require('user-sessions').isRoot()) { + // Check win version: Win7=7600/7601, Win8+>=9200. + // Default to win8+ if unreadable. + var win8plus = true; + try { + var reg = require('win-registry'); + var build = parseInt(reg.QueryKey(reg.HKEY.LocalMachine, 'Software\\Microsoft\\Windows NT\\CurrentVersion', 'CurrentBuildNumber'), 10); + win8plus = !(build > 0 && build < 9200); + } catch (ex) { } + // Win7 lacks the ConversionStatus property, so only select it on win8+ + var q = win8plus ? 'SELECT DeviceID,DriveLetter,ConversionStatus,ProtectionStatus,EncryptionMethod FROM Win32_EncryptableVolume' : 'SELECT DeviceID,DriveLetter,ProtectionStatus,EncryptionMethod FROM Win32_EncryptableVolume'; + var fields = win8plus ? ['DeviceID', 'DriveLetter', 'ConversionStatus', 'ProtectionStatus', 'EncryptionMethod'] : ['DeviceID', 'DriveLetter', 'ProtectionStatus', 'EncryptionMethod']; + // re-use connection instead of creating a new one every time + var sess = wmi.connect(NS_VE); + try { + wmi.query(NS_VE, q, fields, false, 5000) + .forEach(function (vol) { + if (!vol || vol['DriveLetter'] == null) { return; } // shortcut letterless volumes + var drive = vol['DriveLetter'].slice(0, -1); + if (!drives[drive]) { return; } // no matching volume, skip for now. TODO match op DeviceID + try { + var v_id = { DeviceID: vol['DeviceID'] }; // DeviceID(=VolumeKeyProtectorID) is the key needed for the ExecMethod calls + drives[drive].protectionStatus = vol['ProtectionStatus']; + drives[drive].encryptionMethod = vol['EncryptionMethod']; + if (win8plus) { + drives[drive].volumeStatus = vol.ConversionStatus; // property available from the query + } else { + var cs = sess.execMethod('Win32_EncryptableVolume', v_id, 'GetConversionStatus', null, 5000); // win7: no PrecisionFactor param, so no universal get + drives[drive].volumeStatus = (cs && cs.ReturnValue == 0) ? cs.ConversionStatus : 0; } - } - } - ret[drive].identifier = (foundIDMarkedLine ? identifier : ''); // Set Bitlocker Identifier - ret[drive].recoveryPassword = (foundMarkedLine ? password : ''); // Set Bitlocker Password - } catch(ex) { } // just carry on as we cant get bitlocker key - } + // Only retrieve the recovery key on encrypted drives (conv 0 = FullyDecrypted, otherwise some sort of encryption) + if (drives[drive].volumeStatus != 0) { + // Get protectorKey (= identifier). For now only { KeyProtectorType: 3 } (=numerical password=recoverykey). Future possibilities: get all id's and statuses (TPM , Certificate etc) + // https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectors-win32-encryptablevolume + var protKeys = sess.execMethod('Win32_EncryptableVolume', v_id, 'GetKeyProtectors', { KeyProtectorType: 3 }, 5000); + if (protKeys && protKeys.ReturnValue == 0 && Array.isArray(protKeys.VolumeKeyProtectorID) && protKeys.VolumeKeyProtectorID.length > 0) { + drives[drive].identifier = protKeys.VolumeKeyProtectorID[0]; + // Got an id, use it to retrieve the recoverykey + // https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectornumericalpassword-win32-encryptablevolume + var numPass = sess.execMethod('Win32_EncryptableVolume', v_id, 'GetKeyProtectorNumericalPassword', { VolumeKeyProtectorID: protKeys.VolumeKeyProtectorID[0] }, 5000); + // NumericalPassword is null if the volume is locked + if (numPass && numPass.ReturnValue == 0 && numPass.NumericalPassword) { drives[drive].recoveryPassword = numPass.NumericalPassword; } + } + } + } catch (ex) { console.log('win-volumes bitlocker error for ' + drive + ': ' + (ex && ex.message ? ex.message : ex)); } // skip this volume, carry on + }); + } finally { sess.release(); } } - p1._res(ret); - } catch (ex) { p1._res(ret); } // just return volumes as cant get encryption/bitlocker + p1._res({ error: null, drives: drives }); + } catch (e) { console.log('windows_volumes error: ' + (e && e.message ? e.message : e)); p1._res({ error: e, drives: drives }); } // just return what we got + return (p1); } -module.exports = { +module.exports = { getVolumes: function () { try { return (getVolumes()); } catch (x) { return ({}); } }, - volumes_promise: windows_volumes + volumes_promise: windows_volumes_wmi }; \ No newline at end of file diff --git a/agents/modules_meshcore/win-wmi-fixed.js b/agents/modules_meshcore/win-wmi-fixed.js index 60fc18ca00..5dd9592c75 100644 --- a/agents/modules_meshcore/win-wmi-fixed.js +++ b/agents/modules_meshcore/win-wmi-fixed.js @@ -16,15 +16,44 @@ limitations under the License. var promise = require('promise'); var GM = require('_GenericMarshal'); +var COM = require('win-com'); +var sm = require('service-manager'); const CLSID_WbemAdministrativeLocator = '{CB8555CC-9128-11D1-AD9B-00C04FD8FDFF}'; const IID_WbemLocator = '{dc12a687-737f-11cf-884d-00aa004b2e24}'; +const WMI_FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/; const WBEM_FLAG_BIDIRECTIONAL = 0; +const WBEM_FLAG_RETURN_IMMEDIATELY = 0x10; +const WBEM_FLAG_FORWARD_ONLY = 0x20; +const WBEM_FLAG_NONSYSTEM_ONLY = 0x40; +const WBEM_FLAG_CONNECT_USE_MAX_WAIT = 0x80; const WBEM_INFINITE = -1; const WBEM_FLAG_ALWAYS = 0; const E_NOINTERFACE = 0x80004002; +const WBEM_S_NO_ERROR = 0; +const WBEM_S_FALSE = 1; +const WBEM_S_TIMEDOUT = 0x40004; +const WBEM_STATUS_COMPLETE = 0; +const WBEM_ERRORS = { + 0x80041001: 'Generic failure', + 0x80041002: 'Object not found', + 0x80041003: 'Access denied', + 0x80041004: 'Provider failure', + 0x80041006: 'Out of memory', + 0x80041008: 'Invalid parameter', + 0x80041009: 'Resource not available', + 0x8004100E: 'Invalid namespace', + 0x80041010: 'Invalid class', + 0x80041017: 'Invalid query', + 0x80041013: 'Provider not found', + 0x80041021: 'Invalid syntax', + 0x80070422: 'Service unavailable' +}; + var OleAut32 = GM.CreateNativeProxy('OleAut32.dll'); OleAut32.CreateMethod('SafeArrayAccessData'); - +OleAut32.CreateMethod('SafeArrayUnaccessData'); +OleAut32.CreateMethod('SafeArrayDestroy'); +OleAut32.CreateMethod('VariantClear'); var wmi_handlers = {}; const LocatorFunctions = ['QueryInterface', 'AddRef', 'Release', 'ConnectToServer']; @@ -77,6 +106,21 @@ const ResultsFunctions = [ 'Skip' ]; +// +// Reference to IWbemCallResult can be found at: +// https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nn-wbemcli-iwbemcallresult +// Used for semisynchronous calls (WBEM_FLAG_RETURN_IMMEDIATELY): poll GetResultObject() with a timeout. +// +const CallResultFunctions = [ + 'QueryInterface', + 'AddRef', + 'Release', + 'GetResultObject', + 'GetResultString', + 'GetResultServices', + 'GetCallStatus' +]; + // // Reference to IWbemClassObject can be found at: // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nn-wbemcli-iwbemclassobject @@ -111,173 +155,272 @@ const ResultFunctions = [ 'GetMethodOrigin' ]; -// -// Reference to IWbemObjectSink can be found at: // https://learn.microsoft.com/en-us/windows/win32/wmisdk/iwbemobjectsink -// +// A COM interface consists at least of QueryInterface, Addref and Release, which are called by the windows system to manage the COM object +// Indicate gets resultsets pushed and SetStatus gets called when finished +// When refCount hits 0, which normally happens after the SetStatus call through the system calling the Release function, clean everything up const QueryAsyncHandler = [ { - cx: 10, parms: 3, name: 'QueryInterface', func: function (j, riid, ppv) + cx: 10, parms: 3, name: 'QueryInterface', func: function (sink, riid, ppv) { var ret = GM.CreateVariable(4); - console.info1('QueryInterface', riid.Deref(0, 16).toBuffer().toString('hex')); + // console.log('QueryInterface', riid.Deref(0, 16).toBuffer().toString('hex')); switch (riid.Deref(0, 16).toBuffer().toString('hex')) { case '0000000000000000C000000000000046': // IID_IUnknown - j.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer()); - ret.increment(0, true); - //++this.p.refcount; - console.info1('QueryInterface (IID_IUnknown)', this.refcount); - break; case '0178857C8173CF11884D00AA004B2E24': // IID_IWmiObjectSink - j.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer()); - ret.increment(0, true); - //++this.p.refcount; - console.info1('QueryInterface (IID_IWmiObjectSink)', this.refcount); + sink.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer()); + ret.increment(0, true); //return WBEM_S_NO_ERROR=0 + ++this.refcount; break; default: ret.increment(E_NOINTERFACE, true); - console.info1(riid.Deref(0, 16).toBuffer().toString('hex'), 'returning E_NOINTERFACE'); break; } - return (ret); + return ret; } }, { cx: 11, parms: 1, name: 'AddRef', func: function () { - ++this.refcount; - console.info1('AddRef', this.refcount); - return (GM.CreateVariable(4)); + // console.log('AddRef: ' + this.refcount); + return (GM.CreateVariable(4).increment(++this.refcount, true)); // return new refCount } }, { cx: 12, parms: 1, name: 'Release', func: function () { - --this.refcount; - console.info1('Release', this.refcount); - if (this.refcount == 0) - { - console.info1('No More References'); - - this.cleanup(); - this.services.funcs.Release(this.services.Deref()); - - this.services = null; - this.p = null; - if (this.callbackDispatched) - { - setImmediate(function (j) { j.locator = null; }, this); - } - else - { - this.locator = null; - } - - console.info1('No More References [END]'); - } - return (GM.CreateVariable(4)); + // console.log('Release: ' + this.refcount); + if (--this.refcount === 0) { // console.log('Released'); + destroy(this); } + return GM.CreateVariable(4).increment(this.refcount >>> 0, true); // return new refCount } }, { - cx: 13, parms: 3, name: 'Indicate', func: function (j, count, arr) + cx: 13, parms: 3, name: 'Indicate', func: function (sink, count, arr) { - console.info1('Indicate', count.Val); - var j, nme, len, nn; - + // console.log('Indicate: ' + count.Val); + if (this._abandoned) { return (GM.CreateVariable(4).increment(0, true)); } // discard + if (this.sessionid) { + this.progress += count.Val; + var now = Date.now(); + if ((now - this.lastProg) > 2000) { //wait at least 2 seconds per msg, otherwise it gets throttled and possibly lose the result msg + this.lastProg = now; + this.MA.SendCommand({ action: 'msg', type: 'console', value: 'Queryprogress: ' + this.progress + ' results', sessionid: this.sessionid }); } + } for (var i = 0; i < count.Val; ++i) { - j = arr.Deref((i * GM.PointerSize) + 0, GM.PointerSize); - this.results.push(enumerateProperties(j, this.fields)); + var wmiResultObj = arr.Deref((i * GM.PointerSize) + 0, GM.PointerSize); + try { + if (this.reqFields != null) { + this.results.push(enumerateProperties(wmiResultObj, this.reqFields, this.fixedNameVars)); + } else { + var c = this.cache.forRow(wmiResultObj); + this.results.push(enumerateProperties(wmiResultObj, c.propNames, c.propNameVars)); + } + } catch (e) { + // only skip, not throw + console.log('win-wmi Indicate row error: ' + (e && e.message ? e.message : e)); + } } - - var ret = GM.CreateVariable(4); - ret.increment(0, true); - return (ret); + return GM.CreateVariable(4).increment(0, true); //Indicate must always return WBEM_S_NO_ERROR=0 } }, { - cx: 14, parms: 5, name: 'SetStatus', func: function (j, lFlags, hResult, strParam, pObjParam) + cx: 14, parms: 5, name: 'SetStatus', func: function (sink, lFlags, hResult, strParam, pObjParam) { - console.info1('SetStatus', hResult.Val); - - var ret = GM.CreateVariable(4); - ret.increment(0, true); - - if (hResult.Val == 0) - { - this.p.resolve(this.results); - } - else - { - this.p.reject(hResult.Val); + // console.log('SetStatus ' + lFlags.Val); + var ret = GM.CreateVariable(4).increment(0, true); // SetStatus must always return WBEM_S_NO_ERROR=0 + if (lFlags.Val !== WBEM_STATUS_COMPLETE) { return (ret); } // SetStatus can receive other messages, skip those + + if (this._timer) { clearTimeout(this._timer); this._timer = null; } // Finished before the settimeout, remove timer + var self = this; + setImmediate(function () { if (--self.refcount === 0) { destroy(self); } }); // decrement self set refCount of 1 at init + if (this._abandoned) { return (ret); } // timeout settled the promise + this._abandoned = true; // Signal timeout function + if (hResult.Val == 0) { // Everything ok, msg & resolve + if (this.sessionid) { + this.MA.SendCommand({ action: 'msg', type: 'console', value: 'Querytotal: ' + this.results.length + ' results, time take: ' + (Date.now()-this.progStart)/1000 + ' seconds', sessionid: this.sessionid }); + } + this.p.resolve(this.results); } + else { + var e = new Error('WMI async query error: ' + (WBEM_ERRORS[hResult.Val>>>0] || 'unknown') + ' (0x' + (hResult.Val>>>0).toString(16) + ')'); + if (this.results.length > 0) { e.results = this.results; } + this.p.reject(e); } return (ret); } } ]; +function destroy(h) { + // console.log('Destroy oh boy!'); + if (h.cleanup) { h.cleanup(); } + h.p = null; + delete wmi_handlers[h._hashCode()]; + h.services = releaseCOM(h.services, true); + + if (h.callbackDispatched) { + setImmediate(function () { h.locator = releaseCOM(h.locator, false); }); + } else { + h.locator = releaseCOM(h.locator, false); + } +} + +function releaseCOM(obj, deref) { + if (obj && obj.funcs) { + try { obj.funcs.Release(deref ? obj.Deref() : obj); } + catch (e) { console.log('WMI: releaseCOM error = ' + (e && e.message ? e.message : e)); } + } + return null; +} -function enumerateProperties(j, fields) -{ - // - // Reference to SafeArrayAccessData() can be found at: - // https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearrayaccessdata - // +function extractFields(queryString) { + if (typeof queryString !== 'string') { return null; } - var nme, len, nn; - var properties = []; - var values = {}; + // get between 'select' and 'from' + var props = /^\s*SELECT\s+(.+?)\s+FROM\s/i.exec(queryString); + if (props == null) { return null; } - j.funcs = require('win-com').marshalFunctions(j.Deref(), ResultFunctions); + var list = props[1].trim(); + if (list === '*') { return null; } - // First we need to enumerate the COM Array - if (fields != null && Array.isArray(fields)) - { - properties = fields; + var parts = list.split(','); + var fields = []; + for (var i = 0; i < parts.length; ++i) { + var name = parts[i].trim(); + // check if wmi field, otherwise exit, fallback to GetNames in enum + if (!WMI_FIELD.test(name)) { return null; } + fields.push(name); } - else - { - nme = GM.CreatePointer(); - j.funcs.GetNames(j.Deref(), 0, WBEM_FLAG_ALWAYS, 0, nme); - len = nme.Deref().Deref(GM.PointerSize == 8 ? 24 : 16, 4).toBuffer().readUInt32LE(); - nn = GM.CreatePointer(); - OleAut32.SafeArrayAccessData(nme.Deref(), nn); + return (fields.length > 0 ? fields : null); +} + +// Optimze query: If a 'select * from' query has the 'fields' argument, rewrite query to replace the * with fields argument +// Except for event/notification queries (WITHIN or FROM __) +// This gives a significant perfomance boost as enumerateProperties is very costly if it needs to get all properties every row +// Always try to do: queryString='select [field1][,field2][...] from [class]', fields=['field1','field2',...] +// Keeping the fields argument filled helps with the enumerateProperties function not getting all the names every row. +// And the reverse, if there are fields between 'select' and 'from', extract them and return it as the fields argument +function prepareQuery (queryString, fields) { + if (typeof(queryString) !=='string' || queryString.trim().length === 0) { throw new Error('No querystring'); } + // Always check if wmi service is running + var s = sm.manager.getService('winmgmt'); + if (!s.isRunning()) { throw new Error('WMI service not running'); } + if (!Array.isArray(fields) || fields.length === 0) { + fields = extractFields(queryString); } + else { + for (var i = 0; i < fields.length; ++i) { + if (typeof fields[i] !== 'string' || !WMI_FIELD.test(fields[i])) { + throw new Error('win-wmi: invalid field name: ' + fields[i]); + } + } + //skip 'within' clause and system event queries + if ( !(/\bWITHIN\b/i.test(queryString) || /\bFROM\s+__/i.test(queryString))) { + queryString = queryString.replace(/^(\s*SELECT\s+)\*(\s+FROM\s)/i, '$1' + fields.join(',') + '$2'); + } + } + // console.info1('WMI: prepared query = ' + queryString + ' fields: ' + fields); + return { q: queryString, f: fields }; +} +// get all property names for a wmi result +function getAllNames(wmiResultObj, includeSysProp) +{ + if (!wmiResultObj.funcs) { wmiResultObj.funcs = COM.marshalFunctions(wmiResultObj.Deref(), ResultFunctions); } + + var saNames = GM.CreatePointer(); + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-getnames + var res = (wmiResultObj.funcs.GetNames(wmiResultObj.Deref(), 0, (includeSysProp ? WBEM_FLAG_ALWAYS : WBEM_FLAG_NONSYSTEM_ONLY), 0, saNames)); + if (res.Val != 0) { return []; } + var len = saNames.Deref().Deref(GM.PointerSize == 8 ? 24 : 16, 4).toBuffer().readUInt32LE(); + var pNamesArray = GM.CreatePointer(); + OleAut32.SafeArrayAccessData(saNames.Deref(), pNamesArray); + + var propNames = []; + for (var i = 0; i < len; ++i) { + var propName = pNamesArray.Deref().increment(i * GM.PointerSize).Deref().Wide2UTF8; + if (propName.length === 0) { continue; } + propNames.push(propName); + } + OleAut32.SafeArrayUnaccessData(saNames.Deref()); + OleAut32.SafeArrayDestroy(saNames.Deref()); // saNames is caller-owned + // console.info1('WMI: getAllNames(propNames) = ' + JSON.stringify(propNames)); + return propNames; +} - for (var i = 0; i < len; ++i) - { - var propName = nn.Deref().increment(i * GM.PointerSize).Deref().Wide2UTF8; - if (propName.length === 0) { continue; } - properties.push(propName); +function NameCache(includeSysProp) +{ + this.includeSysProp = (typeof(includeSysProp) == 'boolean') ? includeSysProp : false; + this.byClass = {}; + this._classNameVar = GM.CreateVariable('__CLASS', { wide: true }); // reused every row +} + +NameCache.prototype.forRow = function (wmiResultObj) +{ + if (!wmiResultObj.funcs) { wmiResultObj.funcs = COM.marshalFunctions(wmiResultObj.Deref(), ResultFunctions); } + + // Read __CLASS (system property, always a BSTR). Falls back to '' if absent. + var key = ''; + var val = GM.CreateVariable(24); + try { + if (wmiResultObj.funcs.Get(wmiResultObj.Deref(), this._classNameVar, 0, val, 0, 0).Val == 0 && val.toBuffer().readUInt16LE() == 0x0008) { + key = val.Deref(8, GM.PointerSize).Deref().Wide2UTF8; } + } finally { + OleAut32.VariantClear(val); } + + var entry = this.byClass[key]; + if (!entry) { + var propNames = getAllNames(wmiResultObj, this.includeSysProp); + var propNameVars = []; + for (var n = 0; n < propNames.length; ++n) { + propNameVars.push(GM.CreateVariable(propNames[n], { wide: true })); + } + entry = this.byClass[key] = { propNames: propNames, propNameVars: propNameVars }; + } + return entry; +}; + +function enumerateProperties(wmiResultObj, propNames, propNameVars) +{ + // + // Reference to SafeArrayAccessData() can be found at: + // https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearrayaccessdata + // + + var values = {}; + if (!wmiResultObj.funcs) { wmiResultObj.funcs = COM.marshalFunctions(wmiResultObj.Deref(), ResultFunctions); } // Now we need to introspect the Array Fields - for (var i = 0; i < properties.length; ++i) + var propVal = GM.CreateVariable(24); + for (var i = 0; i < propNames.length; ++i) { - var tmp1 = GM.CreateVariable(24); - if (j.funcs.Get(j.Deref(), GM.CreateVariable(properties[i], { wide: true }), 0, tmp1, 0, 0).Val == 0) + if (wmiResultObj.funcs.Get(wmiResultObj.Deref(), propNameVars[i], 0, propVal, 0, 0).Val == 0) { // // Reference for IWbemClassObject::Get() can be found at: // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-get // - var vartype = tmp1.toBuffer().readUInt16LE(); + var vartype = propVal.toBuffer().readUInt16LE(); var isArray = (vartype & 0x2000) != 0; // VT_ARRAY flag var baseType = vartype & 0x0FFF; if (isArray) { // Handle array types (VT_ARRAY | base type) - var safeArray = tmp1.Deref(8, GM.PointerSize).Deref(); + var safeArray = propVal.Deref(8, GM.PointerSize).Deref(); var arrayLength = safeArray.Deref(GM.PointerSize == 8 ? 24 : 16, 4).toBuffer().readUInt32LE(); var arrayData = GM.CreatePointer(); OleAut32.SafeArrayAccessData(safeArray, arrayData); + // VT_I8 & VT_UI8 (64-bit types) aren't used (yet?) in wmi, added for completeness + // added float type VT_R4 and VT_R8, uncommon but used + // VT_DECIMAL not mapped in wmi var arrayValues = []; for (var k = 0; k < arrayLength; ++k) { @@ -290,9 +433,20 @@ function enumerateProperties(j, fields) case 0x0016: // VT_INT arrayValues.push(arrayData.Deref().Deref(k * 4, 4).toBuffer().readInt32LE()); break; + case 0x0004: // VT_R4 + arrayValues.push(arrayData.Deref().Deref(k * 4, 4).toBuffer().readFloatLE(0)); + break; + case 0x0005: // VT_R8 + arrayValues.push(arrayData.Deref().Deref(k * 8, 8).toBuffer().readDoubleLE(0)); + break; + case 0x0008: // VT_BSTR + arrayValues.push(arrayData.Deref().Deref(k * GM.PointerSize, GM.PointerSize).Deref().Wide2UTF8); + break; case 0x000B: // VT_BOOL arrayValues.push(arrayData.Deref().Deref(k * 2, 2).toBuffer().readInt16LE() != 0); break; + // case 0x000E: //VT_DECIMAL + // break; case 0x0010: // VT_I1 arrayValues.push(arrayData.Deref().Deref(k, 1).toBuffer().readInt8()); break; @@ -306,12 +460,24 @@ function enumerateProperties(j, fields) case 0x0017: // VT_UINT arrayValues.push(arrayData.Deref().Deref(k * 4, 4).toBuffer().readUInt32LE()); break; - case 0x0008: // VT_BSTR - arrayValues.push(arrayData.Deref().Deref(k * GM.PointerSize, GM.PointerSize).Deref().Wide2UTF8); +/* + case 0x0014: // VT_I8 (signed 64-bit) + var ai8 = arrayData.Deref().Deref(k * 8, 8).toBuffer(); + // recombine the two 32-bit halves; exact up to 2^53 (high word signed) + arrayValues.push(ai8.readInt32LE(4) * 0x100000000 + ai8.readUInt32LE(0)); + break; + case 0x0015: // VT_UI8 (unsigned 64-bit) + var aui8 = arrayData.Deref().Deref(k * 8, 8).toBuffer(); + arrayValues.push(aui8.readUInt32LE(4) * 0x100000000 + aui8.readUInt32LE(0)); + break; +*/ + default: + console.info1('VARTYPE (array element): 0x' + baseType.toString(16)); break; } } - values[properties[i]] = arrayValues; + OleAut32.SafeArrayUnaccessData(safeArray); + values[propNames[i]] = arrayValues; } else { @@ -320,116 +486,461 @@ function enumerateProperties(j, fields) { case 0x0000: // VT_EMPTY case 0x0001: // VT_NULL - values[properties[i]] = null; + values[propNames[i]] = null; break; case 0x0002: // VT_I2 - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt16LE(); + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt16LE(); break; case 0x0003: // VT_I4 case 0x0016: // VT_INT - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt32LE(); + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt32LE(); break; - case 0x000B: // VT_BOOL - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt32LE() != 0; + case 0x0004: // VT_R4 + values[propNames[i]] = propVal.Deref(8, 4).toBuffer().readFloatLE(0); break; - case 0x000E: // VT_DECIMAL + case 0x0005: // VT_R8 + values[propNames[i]] = propVal.Deref(8, 8).toBuffer().readDoubleLE(0); + break; + case 0x0008: // VT_BSTR + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).Deref().Wide2UTF8; + break; + case 0x000B: // VT_BOOL + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt32LE() != 0; break; + // case 0x000E: // VT_DECIMAL + // break; case 0x0010: // VT_I1 - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt8(); + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt8(); break; case 0x0011: // VT_UI1 - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readUInt8(); + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readUInt8(); break; case 0x0012: // VT_UI2 - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readUInt16LE(); + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readUInt16LE(); break; case 0x0013: // VT_UI4 case 0x0017: // VT_UINT - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readUInt32LE(); + values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readUInt32LE(); break; - //case 0x0014: // VT_I8 - // break; - //case 0x0015: // VT_UI8 - // break; - case 0x0008: // VT_BSTR - values[properties[i]] = tmp1.Deref(8, GM.PointerSize).Deref().Wide2UTF8; +/* + case 0x0014: // VT_I8 + var i8 = propVal.Deref(8, 8).toBuffer(); + // recombine the two 32-bit halves; exact up to 2^53 (high word is signed) + values[propNames[i]] = i8.readInt32LE(4) * 0x100000000 + i8.readUInt32LE(0); + break; + case 0x0015: // VT_UI8 + var ui8 = propVal.Deref(8, 8).toBuffer(); + values[propNames[i]] = ui8.readUInt32LE(4) * 0x100000000 + ui8.readUInt32LE(0); break; +*/ default: - console.info1('VARTYPE: ' + vartype); + console.info1('VARTYPE: 0x' + vartype.toString(16)); break; } } } + OleAut32.VariantClear(propVal); } - return (values); } -function queryAsync(resourceString, queryString, fields) + +// (optional) includeSysProp, default=false. Include system properties (__CLASS, etc.) +// (optional) timeout, default=120 seconds. Query duration timeout in ms +// (optional) sessionid, default=null. Report progress back to the given sessionid. +function queryAsync(resourceString, queryString, fields, includeSysProp, timeout, sessionid) { - var p = new promise(require('promise').defaultInit); - var resource = GM.CreateVariable(resourceString, { wide: true }); - var language = GM.CreateVariable("WQL", { wide: true }); - var query = GM.CreateVariable(queryString, { wide: true }); - var results = GM.CreatePointer(); - - // Setup the Async COM handler for QueryAsync() - var handlers = require('win-com').marshalInterface(QueryAsyncHandler); - handlers.refcount = 1; - handlers.results = []; - handlers.fields = fields; - handlers.locator = require('win-com').createInstance(require('win-com').CLSIDFromString(CLSID_WbemAdministrativeLocator), require('win-com').IID_IUnknown); - handlers.locator.funcs = require('win-com').marshalFunctions(handlers.locator, LocatorFunctions); - - handlers.services = require('_GenericMarshal').CreatePointer(); - if (handlers.locator.funcs.ConnectToServer(handlers.locator, resource, 0, 0, 0, 0, 0, 0, handlers.services).Val != 0) { throw ('Error calling ConnectToService'); } - - handlers.services.funcs = require('win-com').marshalFunctions(handlers.services.Deref(), ServiceFunctions); - handlers.p = p; - - // Make the COM call - if (handlers.services.funcs.ExecQueryAsync(handlers.services.Deref(), language, query, WBEM_FLAG_BIDIRECTIONAL, 0, handlers).Val != 0) - { - throw ('Error in Query'); + var queryStarted = false; + var handlers = null; + try { + var p = new promise(promise.defaultInit); + //32-bit windows cannot do more than 1 async query at a time because of the hardcoded vtable for the cx pre-compiled __stdcall custom handlers in iLibDuktape_GenericMarshal.c + if (GM.PointerSize == 4 && Object.keys(wmi_handlers).length != 0) { + setImmediate(function(){ p.reject(new Error('Another AsyncQuery is already running, only one AsyncQuery possible at a time on 32-bit Windows')); }); return (p); } + if (!timeout || typeof timeout !== 'number') { timeout = 120 * 1000 }; //Default timeout set to 2m + var pq = prepareQuery(queryString, fields); + queryString = pq.q; + var reqFields = pq.f; + var resource = GM.CreateVariable(resourceString, { wide: true }); + var language = GM.CreateVariable("WQL", { wide: true }); + var query = GM.CreateVariable(queryString, { wide: true }); + + // Setup the Async COM handler for QueryAsync() + handlers = COM.marshalInterface(QueryAsyncHandler); + handlers.refcount = 1; + handlers.results = []; + handlers.reqFields = reqFields; + handlers.query = resourceString + ':' + queryString; + handlers._timer = null; + handlers._abandoned = false; + if (reqFields != null && Array.isArray(reqFields)) { + handlers.fixedNameVars = reqFields.map(function (f) { return GM.CreateVariable(f, { wide: true }); }); + } else { + handlers.cache = new NameCache(includeSysProp); + } + if (sessionid) { handlers.sessionid = sessionid; handlers.progStart = Date.now(); handlers.progress = 0; handlers.lastProg = 0; handlers.MA = require('MeshAgent'); } + handlers.locator = COM.createInstance(COM.CLSIDFromString(CLSID_WbemAdministrativeLocator), COM.IID_IUnknown); + handlers.locator.funcs = COM.marshalFunctions(handlers.locator, LocatorFunctions); + + handlers.services = GM.CreatePointer(); + + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemlocator-connectserver + // WBEM_FLAG_CONNECT_USE_MAX_WAIT=wait max 2 minutes instead of infinite. Prevents blocking. + var hr = handlers.locator.funcs.ConnectToServer(handlers.locator, resource, 0, 0, 0, WBEM_FLAG_CONNECT_USE_MAX_WAIT, 0, 0, handlers.services).Val >>> 0; + if (hr != 0) { + throw (new Error('ConnectToServer(' + resourceString + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')')); } + + handlers.services.funcs = COM.marshalFunctions(handlers.services.Deref(), ServiceFunctions); + handlers.p = p; + + // Make the COM call + var execRes = handlers.services.funcs.ExecQueryAsync(handlers.services.Deref(), language, query, WBEM_FLAG_BIDIRECTIONAL, 0, handlers).Val >>> 0; + if (execRes !== 0) { + throw new Error('ExecQueryAsync(' + handlers.query + ') failed: ' + (WBEM_ERRORS[execRes] || 'unknown') + ' (0x' + execRes.toString(16) + ')'); } + queryStarted = true; + // Hold a reference to the callback object + wmi_handlers[handlers._hashCode()] = handlers; + handlers._timer = setTimeout(function () { + handlers._timer = null; + if (handlers._abandoned) { return; } //Signal from SetStatus, it settled + // WMI CancelAsyncCall causes a fatal exception. Only way to cancel is through abandonment. + // Reject the promise and clean that part up, hoping the wmi query resolves eventually and cleans itself up through SetStatus + // There seems to be not other way to stop a hung/long query + handlers._abandoned = true; // Signal SetStatus we'll reject + var e = new Error('WMI query timed out after ' + (timeout/1000) + ' seconds'); + if (handlers.results.length > 0) { e.results = handlers.results; } + handlers.p.reject(e); + }, timeout); + } catch (e) { + console.log('win-wmi queryAsync error: ' + e.message); + if (!queryStarted && handlers) { + handlers.refcount = 0; + destroy(handlers); + } + p.reject(e); } - - // Hold a reference to the callback object - wmi_handlers[handlers._hashCode()] = handlers; return (p); } -function query(resourceString, queryString, fields) + +// (optional) includeSysProp, default=false. Include system properties (__CLASS, etc.) +// (optional) timeout, default=120 seconds. Query duration timeout in ms +// (optional) sessionid, default=null. Report progress back to the given sessionid. +function query(resourceString, queryString, fields, includeSysProp, timeout, sessionid) { - var resource = GM.CreateVariable(resourceString, { wide: true }); - var language = GM.CreateVariable("WQL", { wide: true }); - var query = GM.CreateVariable(queryString, { wide: true }); - var results = GM.CreatePointer(); - - // Connect the locator connection for WMI - var locator = require('win-com').createInstance(require('win-com').CLSIDFromString(CLSID_WbemAdministrativeLocator), require('win-com').IID_IUnknown); - locator.funcs = require('win-com').marshalFunctions(locator, LocatorFunctions); - var services = require('_GenericMarshal').CreatePointer(); - if (locator.funcs.ConnectToServer(locator, resource, 0, 0, 0, 0, 0, 0, services).Val != 0) { throw ('Error calling ConnectToService'); } - - // Execute the Query - services.funcs = require('win-com').marshalFunctions(services.Deref(), ServiceFunctions); - if (services.funcs.ExecQuery(services.Deref(), language, query, WBEM_FLAG_BIDIRECTIONAL, 0, results).Val != 0) { throw ('Error in Query'); } - - results.funcs = require('win-com').marshalFunctions(results.Deref(), ResultsFunctions); - var returnedCount = GM.CreateVariable(8); - var result = GM.CreatePointer(); + const uCount = 32; // number of rows to retrieve per round var ret = []; + try { + if (!timeout || typeof timeout !== 'number') { timeout = 120 * 1000 }; //Default timeout set to 2m + var pq = prepareQuery(queryString, fields); + queryString = pq.q; + var reqFields = pq.f; + var fixedNameVars = null; + var cache = null; + if (reqFields != null && Array.isArray(reqFields)) { + fixedNameVars = reqFields.map(function (f) { return GM.CreateVariable(f, { wide: true }); }); + } else { + cache = new NameCache(includeSysProp); + } + var resource = GM.CreateVariable(resourceString, { wide: true }); + var language = GM.CreateVariable("WQL", { wide: true }); + var query = GM.CreateVariable(queryString, { wide: true }); + var results = GM.CreatePointer(); + if (sessionid) { var MA = require('MeshAgent'), progress = 0, lastProg = 0 } + // Connect the locator connection for WMI + var locator = COM.createInstance(COM.CLSIDFromString(CLSID_WbemAdministrativeLocator), COM.IID_IUnknown); + locator.funcs = COM.marshalFunctions(locator, LocatorFunctions); + var services = GM.CreatePointer(); + + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemlocator-connectserver + // WBEM_FLAG_CONNECT_USE_MAX_WAIT=wait max 2 minutes instead of infinite. Prevents blocking. + var hr = locator.funcs.ConnectToServer(locator, resource, 0, 0, 0, WBEM_FLAG_CONNECT_USE_MAX_WAIT, 0, 0, services).Val >>> 0; + if (hr != 0) { + throw new Error('ConnectToServer(' + resourceString + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); } + + // Execute the Query + // FORWARD_ONLY & RETURN_IMMEDIATELY instead of BIDIRECTIONAL, faster and less memory. https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-execquery + services.funcs = COM.marshalFunctions(services.Deref(), ServiceFunctions); + if ((hr = services.funcs.ExecQuery(services.Deref(), language, query, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, 0, results).Val >>> 0) != 0) { + throw new Error('ExecQuery() failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); } + + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-ienumwbemclassobject-next + // Batch retrieve per round instead of the original 1. Improves speed on larger sets as it lowers COM calls, like the asyncHandler does + // nextRes=WBEM_S_FALSE: signals the last set. Check returnedCount if rows left to process + // nextRes=WBEM_S_NO_ERROR: returnedCount equal to requested number + results.funcs = COM.marshalFunctions(results.Deref(), ResultsFunctions); + var apObjects = GM.CreateVariable(uCount * GM.PointerSize); + var returnedCount = GM.CreateVariable(4); // ULONG puReturned + var retries = 0, rowTimeout = 10 * 1000; // rowTimeout was WBEM_INFINITE. in ms. Prevents blocking. Generates WBEM_S_TIMEDOUT on timeout + var progStart = Date.now(); + while (true) { + if ((Date.now()-progStart) > timeout) { throw new Error('WMI query timed out after ' + (timeout/1000) + ' seconds'); } + var nextRes = results.funcs.Next(results.Deref(), rowTimeout, uCount, apObjects, returnedCount).Val >>> 0; + if (nextRes >= 0x80000000) { throw new Error('Next() failed: ' + (WBEM_ERRORS[nextRes] || 'unknown') + ' (0x' + nextRes.toString(16) + ')'); } + var returnedRows = (nextRes === WBEM_S_NO_ERROR) ? uCount : returnedCount.toBuffer().readUInt32LE(); // Only use returnedCount on last set, as WBEM_S_NO_ERROR implies a full batch + + if (returnedRows === 0) { // handle special case, no rows returned + if (nextRes === WBEM_S_FALSE) { break; } // Done. Exit. + if (++retries > 6) { throw new Error('Next() errored too many times: ' + (WBEM_ERRORS[nextRes] || 'unknown') + ' (0x' + nextRes.toString(16) + ')'); } + if (sessionid) { MA.SendCommand({ action: 'msg', type: 'console', value: 'Queryprogress: issue ' + (WBEM_ERRORS[nextRes] || 'unknown') + ' (0x' + nextRes.toString(16) + ') after ' + progress + ' results', sessionid: sessionid }); } + continue; // timed out, retry + } + retries = 0; + + for (var row = 0; row < returnedRows; ++row) { + var rowObj = apObjects.Deref(row * GM.PointerSize, GM.PointerSize); + rowObj.funcs = COM.marshalFunctions(rowObj.Deref(), ResultFunctions); + try { + if (reqFields != null) { ret.push(enumerateProperties(rowObj, reqFields, fixedNameVars)); } + else { var c = cache.forRow(rowObj); ret.push(enumerateProperties(rowObj, c.propNames, c.propNameVars)); } + } catch (rowErr) { + console.log('win-wmi query row error: ' + (rowErr && rowErr.message ? rowErr.message : rowErr)); + } finally { + rowObj.funcs.Release(rowObj.Deref()); + } + } - // Enumerate the results - while (results.funcs.Next(results.Deref(), WBEM_INFINITE, 1, result, returnedCount).Val == 0) - { - ret.push(enumerateProperties(result, fields)); + if (sessionid) { + progress += returnedRows; + var now = Date.now(); + if ((now - lastProg) > 2000) { + lastProg = now; + MA.SendCommand({ action: 'msg', type: 'console', value: 'Queryprogress: ' + progress + ' results', sessionid: sessionid }); + } + } + + if (nextRes === WBEM_S_FALSE) { break; } // last (partial) batch done. Exit + } + } catch (e) { + console.log('win-wmi query error: ' + e.message); + if (ret.length > 0) { e.results = ret; } + throw (e); + } finally { + results = releaseCOM(results, true); + services = releaseCOM(services, true); + locator = releaseCOM(locator, false); + } + // console.log(JSON.stringify(ret).substring(0,200)); + if (sessionid) { + MA.SendCommand({ action: 'msg', type: 'console', value: 'Querytotal: ' + ret.length + ' results, time take: ' + (Date.now()-progStart)/1000 + ' seconds', sessionid: sessionid }); } + return (ret); +} + +// Helperfunction to avoid annoying backslash/double quote escapes +function buildEscapePath(className, keys) { + if (!keys || Object.keys(keys).length === 0) { return className; } + var parts = []; + for (var k in keys) { + var val = keys[k]; + // Numeric keys are written as is, the rest gets an escape + if (typeof val === 'number') { parts.push(k + '=' + val); } + else { parts.push(k + '="' + ('' + val).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'); } } + return className + '.' + parts.join(','); +} - results.funcs.Release(results.Deref()); - services.funcs.Release(services.Deref()); - locator.funcs.Release(locator); +// Write a single input value into a VARIANT buffer (vbuf = the variant's toBuffer() view). +// !!! Returns a GM variable that must be KEPT ALIVE until AFTER the COM call (for VT_BSTR only, whose pointer lives in the variant), +// This is because the VARIANT does not contain the string bytes. It contains an 8-byte pointer to bstr's separately-allocated native memory. +// The VARIANT and the string buffer are two different allocations, so the caller must keep a reference until after the COM call. +// This prevents the GC from cleaning up the buffer allocation until we're done. +function writeInParamVariant(vbuf, val) { + var type = null, value = val; + if (val !== null && typeof val === 'object' && val.type != null) { type = ('' + val.type).toLowerCase(); value = val.value; } + if (type === null) { + if (val === null || typeof val === 'undefined') { type = 'null'; } + else if (typeof val === 'boolean') { type = 'bool'; } + else if (typeof val === 'string') { type = 'bstr'; } + else if (typeof val === 'number') { type = (Math.floor(value) === value) ? 'i4' : 'r8'; } // integer -> VT_I4, fractional -> VT_R8 + else { throw new Error('win-wmi: unsupported inParam value: ' + JSON.stringify(val)); } + } + switch (type) { + case 'empty': vbuf.writeUInt16LE(0x0000, 0); break; // VT_EMPTY + case 'null': vbuf.writeUInt16LE(0x0001, 0); break; // VT_NULL + case 'i2': case 'int16': case 'short':vbuf.writeUInt16LE(0x0002, 0); vbuf.writeInt16LE(value | 0, 8); break; // VT_I2 + case 'i4': case 'int32': case 'int': vbuf.writeUInt16LE(0x0003, 0); vbuf.writeInt32LE(value | 0, 8); break; // VT_I4 + case 'r4': case 'float': vbuf.writeUInt16LE(0x0004, 0); vbuf.writeFloatLE(+value, 8); break; // VT_R4 + case 'r8': case 'double': vbuf.writeUInt16LE(0x0005, 0); vbuf.writeDoubleLE(+value, 8); break; // VT_R8 + case 'bool': case 'boolean': vbuf.writeUInt16LE(0x000B, 0); vbuf.writeInt16LE(value ? -1 : 0, 8); break; // VT_BOOL (-1 = true) + case 'i1': case 'int8': case 'sbyte': vbuf.writeUInt16LE(0x0010, 0); vbuf.writeInt8(value | 0, 8); break; // VT_I1 + case 'ui1': case 'uint8': case 'byte': vbuf.writeUInt16LE(0x0011, 0); vbuf.writeUInt8(value & 0xFF, 8);break; // VT_UI1 + case 'ui2': case 'uint16': vbuf.writeUInt16LE(0x0012, 0); vbuf.writeUInt16LE(value & 0xFFFF, 8); break; // VT_UI2 + case 'ui4': case 'uint32': case 'uint': vbuf.writeUInt16LE(0x0013, 0); vbuf.writeUInt32LE(value >>> 0, 8); break; // VT_UI4 + case 'bstr': case 'string': // VT_BSTR + var bstr = GM.CreateVariable('' + value, { wide: true }); + vbuf.writeUInt16LE(0x0008, 0); + bstr.pointerBuffer().copy(vbuf, 8); // pointer to wide string at union offset 8 + return bstr; // caller must keep this alive so the GC won't reclaim + default: throw new Error('win-wmi: unsupported inParam VARIANT type: ' + type); + } + return null; // directly written, no need to keep alive +} - return (ret); +// Open a reusable connection to a WMI namespace, so multiple ExecMethod calls can share a single +// IWbemServices and skip the per-call ConnectToServer (the expensive DCOM/RPC handshake). Caller MUST +// call release() when done. +// var sess = wmi.connect(ns); +// try { sess.execMethod(className, keys, method, inParams, timeout); ... } finally { sess.release(); } +function wmiConnect(resourceString) +{ + var s = sm.manager.getService('winmgmt'); + if (!s.isRunning()) { throw new Error('WMI service not running'); } + + var locator = COM.createInstance(COM.CLSIDFromString(CLSID_WbemAdministrativeLocator), COM.IID_IUnknown); + locator.funcs = COM.marshalFunctions(locator, LocatorFunctions); + var services = GM.CreatePointer(); + var resource = GM.CreateVariable(resourceString, { wide: true }); + var hr = locator.funcs.ConnectToServer(locator, resource, 0, 0, 0, WBEM_FLAG_CONNECT_USE_MAX_WAIT, 0, 0, services).Val >>> 0; + if (hr != 0) { + releaseCOM(locator, false); + throw new Error('ConnectToServer(' + resourceString + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); + } + services.funcs = COM.marshalFunctions(services.Deref(), ServiceFunctions); + + return { + ns: resourceString, + services: services, + locator: locator, + _sigCache: {}, // 'Class.Method' -> in-params signature (IWbemClassObject), reused across calls + // Same signature as execMethod() minus the leading namespace (it's bound to this connection) + execMethod: function (className, keys, methodName, inParams, timeout) { + return execMethodOn(this.services, this.ns, className, keys, methodName, inParams, timeout, this._sigCache); + }, + release: function () { + for (var k in this._sigCache) { this._sigCache[k] = releaseCOM(this._sigCache[k], true); } + this.services = releaseCOM(this.services, true); + this.locator = releaseCOM(this.locator, false); + } + }; +} + +// Call a WMI method via IWbemServices::ExecMethod +// resourceString : namespace, e.g. 'root\\cimv2\\Security\\MicrosoftVolumeEncryption' +// className : the class the method lives on, e.g. 'Win32_EncryptableVolume' +// keys : (optional) plain object of key property/properties identifying the +// instance, e.g. { DeviceID: vols[0].DeviceID }. Values are escaped +// automatically - pass the RAW value, do NOT pre-escape. Pass null/{} +// for static (class-level) methods. +// methodName : e.g. 'GetConversionStatus' +// inParams : (optional) plain object of { paramName: value } input arguments +// timeout : (optional) max ms to wait for the method to complete, default 60s. +// +// One-shot: connects, runs, releases. For several calls to the same namespace use wmi.connect(ns) +// and reuse sess.execMethod(...) to avoid reconnecting each time. +// Reference: https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-execmethod +function execMethod(resourceString, className, keys, methodName, inParams, timeout) +{ + var sess = wmiConnect(resourceString); + try { return execMethodOn(sess.services, resourceString, className, keys, methodName, inParams, timeout); } + finally { sess.release(); } +} + +// Core ExecMethod against an already-connected IWbemServices. Does NOT release `services`/locator - +// the caller (execMethod wrapper or a wmiConnect() session) owns the connection lifetime. +// sigCache (optional): a session-owned { 'Class.Method': inSignature } map. When passed, the in-params +// signature is fetched once (GetObject+GetMethod) and reused on subsequent calls; the session releases it. +function execMethodOn(services, ns, className, keys, methodName, inParams, timeout, sigCache) +{ + if (!timeout || typeof timeout !== 'number') { timeout = 60 * 1000; } // default 60s ceiling + var debug = false; + function DBG(msg) { if (debug) { console.log('win-wmiii execMethod: ' + msg); } } + var objectPath = buildEscapePath(className, keys); + DBG('ns=' + ns + ' path=' + objectPath + ' method=' + methodName + ' in=' + JSON.stringify(inParams || {})); + var classObj = null, inSig = null, inSigCached = false, inInst = null, outParams = null, callRes = null; + try { + var hr; + var pathVar = GM.CreateVariable(objectPath, { wide: true }); + var methodVar = GM.CreateVariable(methodName, { wide: true }); + + // Get the in-params signature (cached per Class.Method when a session is used) > spawn instance > put params in + var pInParams = 0; + if (inParams && Object.keys(inParams).length > 0) { + var sigKey = className + '.' + methodName; + if (sigCache && sigCache[sigKey]) { + inSig = sigCache[sigKey]; // reuse the cached signature, skip GetObject/GetMethod + inSigCached = true; + } else { + // Get the class definition, then the method's in-signature + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-getobject + var classNameVar = GM.CreateVariable(className, { wide: true }); + classObj = GM.CreatePointer(); + if ((hr = services.funcs.GetObject(services.Deref(), classNameVar, 0, 0, classObj, 0).Val >>> 0) != 0) { + throw new Error('GetObject(' + className + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); } + classObj.funcs = COM.marshalFunctions(classObj.Deref(), ResultFunctions); + + // GetMethod -> in-param signature (ppOutSignature is optional, pass 0 - we don't use it) + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-getmethod + inSig = GM.CreatePointer(); + if ((hr = classObj.funcs.GetMethod(classObj.Deref(), methodVar, 0, inSig, 0).Val >>> 0) != 0) { + throw new Error('GetMethod(' + methodName + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); } + inSig.funcs = COM.marshalFunctions(inSig.Deref(), ResultFunctions); + if (sigCache) { sigCache[sigKey] = inSig; inSigCached = true; } // hand ownership to the session cache + } + + // create class instance to put params in + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-spawninstance + inInst = GM.CreatePointer(); + if ((hr = inSig.funcs.SpawnInstance(inSig.Deref(), 0, inInst).Val >>> 0) != 0) { + throw new Error('SpawnInstance failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); } + inInst.funcs = COM.marshalFunctions(inInst.Deref(), ResultFunctions); + + // Set the named properties in the instance + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-put + var keepAlive = []; + for (var name in inParams) { + var nameVar = GM.CreateVariable(name, { wide: true }); + var v = GM.CreateVariable(24); // VARIANT: 16 bytes on x86 / 24 on x64; same offset, so doesn't have to be 32/64 bit specific, use largest + var vbuf = v.toBuffer(); + var val = inParams[name]; + var keep = writeInParamVariant(vbuf, val); + if (keep != null) { keepAlive.push(keep); } // Keep the B_STR reference alive until at least after the COM call, now releases after function exit + DBG('Put ' + name + '=' + JSON.stringify(val) + ' variant[0:12]=' + vbuf.slice(0, 12).toString('hex')); + if ((hr = inInst.funcs.Put(inInst.Deref(), nameVar, 0, v, 0).Val >>> 0) != 0) { + throw new Error('Put(' + name + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); } + } + pInParams = inInst.Deref(); + } + + // Run execMethod semi-synchronous to prevent blocking + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-execmethod + DBG('ExecMethod calling, pInParams=' + (pInParams ? 'set' : 'NULL')); + callRes = GM.CreatePointer(); + hr = services.funcs.ExecMethod(services.Deref(), pathVar, methodVar, WBEM_FLAG_RETURN_IMMEDIATELY, 0, pInParams, 0, callRes).Val >>> 0; + DBG('ExecMethod returned 0x' + hr.toString(16) + (hr ? ' (' + (WBEM_ERRORS[hr] || 'unknown') + ')' : '')); + if (hr != 0) { + throw new Error('ExecMethod(' + methodName + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); } + callRes.funcs = COM.marshalFunctions(callRes.Deref(), CallResultFunctions); + + // GetResultObject returns WBEM_S_TIMEDOUT if the method hasn't finished within the chunk. + // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemcallresult-getresultobject + outParams = GM.CreatePointer(); // results + var deadline = Date.now() + timeout; + var chunk = 2000; // ms per GetResultObject() call + while (true) { + if (Date.now() > deadline) { throw new Error('ExecMethod(' + methodName + ') timed out after ' + (timeout/1000) + ' seconds'); } + var gr = callRes.funcs.GetResultObject(callRes.Deref(), chunk, outParams).Val >>> 0; + if (gr === WBEM_S_NO_ERROR) { break; } // ready + if (gr === WBEM_S_TIMEDOUT) { continue; } //continue until deadline + throw new Error('GetResultObject(' + methodName + ') failed: ' + (WBEM_ERRORS[gr] || 'unknown') + ' (0x' + gr.toString(16) + ')'); + } + + if (outParams.Deref().Val == 0) { DBG('no out-params returned'); return {}; } // method returned no out-params + outParams.funcs = COM.marshalFunctions(outParams.Deref(), ResultFunctions); + // Single result object: read its property names directly (no need for NameCache's per-class caching / __CLASS read) + var propNames = getAllNames(outParams, false); + var propNameVars = []; + for (var pi = 0; pi < propNames.length; ++pi) { propNameVars.push(GM.CreateVariable(propNames[pi], { wide: true })); } + var out = enumerateProperties(outParams, propNames, propNameVars); + DBG('out=' + JSON.stringify(out) + '\r\n'); + return out; + } finally { + // Release only the per-call COM objects. services/locator are owned by the caller (wrapper or session). + // inSig is released here only when NOT cached - a cached signature is owned by the session and freed in release(). + outParams = releaseCOM(outParams, true); + callRes = releaseCOM(callRes, true); + inInst = releaseCOM(inInst, true); + if (!inSigCached) { inSig = releaseCOM(inSig, true); } + classObj = releaseCOM(classObj, true); + } } -module.exports = { query: query, queryAsync: queryAsync }; +module.exports = { query: query, queryAsync: queryAsync, execMethod: execMethod, connect: wmiConnect }; \ No newline at end of file diff --git a/amtmanager.js b/amtmanager.js index 24e76e0298..a67068f8af 100644 --- a/amtmanager.js +++ b/amtmanager.js @@ -2892,7 +2892,7 @@ module.exports.CreateAmtManager = function (parent) { // See what admin password to use dev.aquired.user = 'admin'; - dev.aquired.pass = dev.temp.password; + dev.aquired.pass = dev.temp.pass; // Set the account password if (typeof dev.temp.mebxpass == 'string') { diff --git a/amtprovisioningserver.js b/amtprovisioningserver.js index d31b7b8387..ae9bd71c9c 100644 --- a/amtprovisioningserver.js +++ b/amtprovisioningserver.js @@ -479,7 +479,22 @@ module.exports.CreateAmtProvisioningServer = function (parent, config) { // Save activation data to amtactivation.log var domain = parent.config.domains[dev.domainid]; - obj.logAmtActivation(domain, { time: new Date(), action: 'acmactivate-bare-metal', domain: dev.domainid, amtUuid: dev.guid, newmebx: config.newmebxpassword, mesh: dev.meshid, amtRealm: dev.aquired.realm, amtver: dev.aquired.version, host: dev.aquired.host, ip: dev.addr, user: dev.aquired.user, pass: dev.aquired.pass, tls: dev.aquired.tls, tlshash: dev.aquired.hash }); + obj.logAmtActivation(domain, { + time: new Date(), + action: 'acmactivate-bare-metal', + domain: dev.domainid, + amtUuid: dev.guid, + newmebx: config.newmebxpassword, + mesh: dev.meshid, + amtRealm: dev.aquired.realm, + amtver: dev.aquired.version, + host: dev.aquired.host, + ip: dev.addr, + user: dev.aquired.user, + password: dev.aquired.pass, + tls: dev.aquired.tls, + tlshash: dev.aquired.hash + }); // Update device in the database if (UpdateDevice(dev) == false) return; diff --git a/amtscanner.js b/amtscanner.js index ae558f8af2..f376172d08 100644 --- a/amtscanner.js +++ b/amtscanner.js @@ -169,7 +169,7 @@ module.exports.CreateAmtScanner = function (parent) { var scaninfo = obj.scanTable[doc._id]; if (scaninfo == null) { var tag = obj.nextTag++; - obj.scanTableTags[tag] = obj.scanTable[doc._id] = scaninfo = { nodeinfo: doc, present: true, tag: tag, state: 0 }; + obj.scanTableTags[tag] = obj.scanTable[doc._id] = scaninfo = { nodeinfo: doc, present: true, tag: tag, state: 0, lastpong: 0 }; //console.log('Scan ' + host + ', state=' + scaninfo.state + ', delta=' + delta); } else { scaninfo.present = true; @@ -208,6 +208,15 @@ module.exports.CreateAmtScanner = function (parent) { delete obj.scanTable[i]; } } + // Close any UDP servers whose serverid is no longer referenced by any active tag + var activeServerIds = {}; + for (var t in obj.scanTableTags) { activeServerIds[Math.floor(parseInt(t) / 255)] = true; } + for (var sid in obj.servers) { + if (activeServerIds[parseInt(sid)] !== true) { + try { obj.servers[sid].close(); } catch (e) { } + delete obj.servers[sid]; + } + } }); return true; }; @@ -252,18 +261,19 @@ module.exports.CreateAmtScanner = function (parent) { } }; - // Send a pending RMCP packet + // Send pending RMCP packets - process a batch per tick to avoid queue buildup with large device counts obj.sendPendingPacket = function () { - try { + var batchSize = 10; + var count = 0; + while (count++ < batchSize) { var p = obj.pendingSends.shift(); - if (p != undefined) { - p[0].send(p[1], 623, p[2]); - p[0].send(p[1], 623, p[2]); - } else { + if (p == undefined) { clearInterval(obj.pendingSendTimer); obj.pendingSendTimer = null; + break; } - } catch (e) { } + try { p[0].send(p[1], 623, p[2]); p[0].send(p[1], 623, p[2]); } catch (e) { } + } }; // Parse RMCP packet diff --git a/apprelays.js b/apprelays.js index 03ea8058e6..e1939eb544 100644 --- a/apprelays.js +++ b/apprelays.js @@ -57,6 +57,7 @@ const MESHRIGHT_RESETOFF = 0x00040000; // 262144 const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288 const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576 const MESHRIGHT_RELAY = 0x00200000; // 2097152 +const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304 const MESHRIGHT_ADMIN = 0xFFFFFFFF; // SerialTunnel object is used to embed TLS within another connection. @@ -531,6 +532,11 @@ module.exports.CreateWebRelay = function (parent, db, args, domain, mtype) { obj.socketContentLengthRemaining = 0; function processHttpData(data) { //console.log('processHttpData', data.length); + // If already in streaming mode, forward directly without accumulating to avoid memory leaks. This is used for streaming responses like SSE (Server-Sent Events). + if (obj.isStreaming) { + try { obj.res.write(data, 'binary'); } catch (ex) { } + return; + } obj.socketAccumulator += data; while (true) { //console.log('ACC(' + obj.socketAccumulator + '): ' + obj.socketAccumulator); @@ -556,8 +562,15 @@ module.exports.CreateWebRelay = function (parent, db, args, domain, mtype) { // Check if this is a streaming response if ((obj.socketXHeader['content-type'] != null) && (obj.socketXHeader['content-type'].toLowerCase().indexOf('text/event-stream') >= 0)) { - obj.isStreaming = true; // This tunnel is now a streaming tunnel and will not close anytime soon. - if (obj.onNextRequest != null) obj.onNextRequest(); // Call this so that any HTTP requests that are waitting for this one to finish get handled by a new tunnel. + obj.isStreaming = true; + if (obj.onNextRequest != null) obj.onNextRequest(); + // Forward headers, then clear accumulator and switch to direct forward mode + processHttpResponse(obj.socketXHeader, null, false); + if (obj.socketAccumulator.length > 0) { + try { obj.res.write(obj.socketAccumulator, 'binary'); } catch (ex) { } + obj.socketAccumulator = ''; // Free the memory to avoid memory leaks + } + return; // Exit the while loop, future data handled directly } // Check if this HTTP request has a body diff --git a/backup.js b/backup.js new file mode 100644 index 0000000000..d18afc7253 --- /dev/null +++ b/backup.js @@ -0,0 +1,107 @@ +// backup to zipfile +module.exports.zipDirectory = async function (dirPath, outputZip, password = '', compression = 5) { + try { + const { ZipWriterStream } = require("@zip.js/zip.js"); + const { readdir, stat } = require("fs/promises"); + const path = require("path"); + const { createReadStream, createWriteStream } = require("fs"); + const { Readable } = require("stream"); + const { pipeline } = require("stream/promises"); + + const zipStream = new ZipWriterStream({ + ...(password ? { password, encryptionStrength: 3 } : {}), // 3 = AES-256 + level: compression + // msDosCompatible: true // to write EntryMetaData#externalFileAttributes in MS-DOS format for folder entries. Fixes Dir entries issue + }); + + async function addFiles() { + const entries = await readdir(dirPath, { recursive: true, withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(entry.parentPath, entry.name); + const relPath = path.relative(dirPath, fullPath).replaceAll(path.sep, path.posix.sep); + + if (entry.isDirectory()) { + await zipStream.zipWriter.add(relPath + '/', null); + } else { + await Readable.toWeb(createReadStream(fullPath)).pipeTo(zipStream.writable(relPath)); + } + // console.log ("zip addfile: " + entry); + } + } + + await Promise.all([ + addFiles().then(() => zipStream.close()), + pipeline(Readable.fromWeb(zipStream.readable), createWriteStream(outputZip)), + ]); + return { res: true, mes: 'Zip created succesfully' }; + } catch (e) { + // console.error(e); + return { res: false, mes: e.message }; + } +} + +// restore zipfile +// removePath: optional path to remove from destination path +// First check archive on illegal paths (updirs and absolute paths), then extract +module.exports.zipExtract = async function (zipPath, destPath, removePath = '', password = "") { + try { + const { ZipReader, Uint8ArrayReader } = require("@zip.js/zip.js"); + const { createWriteStream } = require("fs"); + const { readFile, mkdir } = require("fs/promises"); + const path = require("path"); + const { Readable } = require("stream"); + const { pipeline } = require("stream/promises"); + + const zipReader = new ZipReader(new Uint8ArrayReader(await readFile(zipPath))); + const entries = await zipReader.getEntries(); + const resolveDestPath = path.resolve(destPath); + + // Archive validation: Abort the restore on any bad path before extraction + const targets = []; // { entry, fullPath } cache the paths while we're at it + for (const entry of entries) { + // skip symlinks in unix-mode zips (upper 16 bits hold st_mode); not present in dos/win zips + if (((entry.externalFileAttributes >>> 16) & 0xF000) === 0xA000) { continue; } + + // anchored prefix strip — must match what the extraction pass writes + let name = entry.filename; + if (removePath && name.startsWith(removePath)) name = name.slice(removePath.length); + + // reject any parent-dir token outright (split on both separators; segment match, not substring) + if (name.split(/[/\\]/).includes('..')) { + throw new Error('Aborting, nothing restored. Illegal path (parent traversal) in zip, entry: ' + entry.filename); + } + // reject absolute paths (posix root, or Windows drive / UNC) + if (path.isAbsolute(name) || /^[a-zA-Z]:/.test(name) || name.startsWith('\\')) { + throw new Error('Aborting, nothing restored. Illegal absolute path in zip, entry: ' + entry.filename); + } + + // backstop: resolved target must stay inside destPath + const fullPath = path.resolve(destPath, name); + if (fullPath !== resolveDestPath && !fullPath.startsWith(resolveDestPath + path.sep)) { + throw new Error('Illegal path in zip entry: ' + entry.filename); + } + + targets.push({ entry, fullPath }); + } + + // Validated, extract! + for (const { entry, fullPath } of targets) { + if (entry.directory) { + await mkdir(fullPath, { recursive: true }); + } else { + await mkdir(path.dirname(fullPath), { recursive: true }); + const { writable, readable } = new TransformStream(); + await Promise.all([ + entry.getData(writable, { password }), + pipeline(Readable.fromWeb(readable), createWriteStream(fullPath)), + ]); + } + } + + await zipReader.close(); + return { res: true, mes: 'Extraction successful' }; + } catch (e) { + // console.error(e.message); + return { res: false, mes: e.message }; + } +} \ No newline at end of file diff --git a/certoperations.js b/certoperations.js index bc3234f2b4..6c8da72412 100644 --- a/certoperations.js +++ b/certoperations.js @@ -518,16 +518,23 @@ module.exports.CertificateOperations = function (parent) { // Return a text file from a remote HTTPS server obj.loadTextFile = function (url, tag, func) { - const u = require('url').parse(url); + const u = new URL(url); if (u.protocol == 'https:') { // Read from HTTPS const https = require('https'); - https.get(url, function(resp) { + const options = { timeout: 10000 }; + if (process.env['HTTPS_PROXY'] || process.env['HTTP_PROXY'] || process.env['https_proxy'] || process.env['http_proxy']) { + options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(process.env['HTTPS_PROXY'] || process.env['HTTP_PROXY'] || process.env['https_proxy'] || process.env['http_proxy']); + } + const req = https.get(url, options, function(resp) { + if (resp.statusCode < 200 || resp.statusCode >= 300) { resp.resume(); func(url, null, tag); return; } var data = ''; resp.on('data', function(chunk) { data += chunk; }); - resp.on('end', function () { func(url, data, tag); }); - resp.on('error', function (chunk) { func(url, null, tag); }); - }).on('error', function (err) { func(url, null, tag); }); + resp.on('end', function() { func(url, data, tag); }); + resp.on('error', function() { func(url, null, tag); }); + }); + req.on('error', function() { func(url, null, tag); }); + req.on('timeout', function() { req.destroy(); func(url, null, tag); }); } else if (u.protocol == 'file:') { // Read a file obj.fs.readFile(url.substring(7), 'utf8', function (err, data) { @@ -538,7 +545,7 @@ module.exports.CertificateOperations = function (parent) { // Return the certificate of the remote HTTPS server obj.loadCertificate = function (url, hostname, tag, func) { - const u = require('url').parse(url); + const u = new URL(url); if (u.protocol == 'https:') { // Read the certificate from HTTPS if (hostname == null) { hostname = u.hostname; } diff --git a/db.js b/db.js index 2d6eb35db3..1af81a9695 100644 --- a/db.js +++ b/db.js @@ -42,9 +42,9 @@ module.exports.CreateDB = function (parent, func) { const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental']; const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra']; obj.sqliteConfig = { - maintenance: '', + maintenance: 'PRAGMA optimize;', startupVacuum: false, - autoVacuum: 'full', + autoVacuum: 'incremental', incrementalVacuum: 100, journalMode: 'delete', journalSize: 4096000, @@ -241,7 +241,7 @@ module.exports.CreateDB = function (parent, func) { obj.Remove('si' + node._id); // Remove system information obj.Remove('al' + node._id); // Remove error log last time if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data - obj.RemoveAllNodeEvents(node._id); // Remove all events for this node + obj.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token obj.Get('ra' + node._id, function (err, nodes) { @@ -267,7 +267,7 @@ module.exports.CreateDB = function (parent, func) { parent.DispatchEvent(targets, obj, event); } } else if (i.startsWith('ugrp/')) { - var cusergroup = parent.userGroups[i]; + var cusergroup = parent.webserver.userGroups[i]; if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) { // Remove the user link & save the user delete cusergroup.links[node._id]; @@ -454,7 +454,7 @@ module.exports.CreateDB = function (parent, func) { } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres - sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { }); + sqlDbQuery('DELETE FROM main WHERE extra LIKE \'mesh/%\' AND extra <> ALL ($1)', [meshlist], function (err, response) { }); } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { }); @@ -768,6 +768,7 @@ module.exports.CreateDB = function (parent, func) { sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { }); sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { }); sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { }); + sqlDbExec('CREATE INDEX ndxeventidsfkid ON eventids(fkid)', null, function (err, response) { }); sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { }); sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { }); sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { }); @@ -783,23 +784,26 @@ module.exports.CreateDB = function (parent, func) { // SQLite3 database setup obj.databaseType = DB_SQLITE; const sqlite3 = require('sqlite3'); - let configParams = parent.config.settings.sqlite3; - if (typeof configParams == 'string') {databaseName = configParams} else {databaseName = configParams.name ? configParams.name : 'meshcentral';}; - obj.sqliteConfig.startupVacuum = configParams.startupvacuum ? configParams.startupvacuum : false; - obj.sqliteConfig.autoVacuum = configParams.autovacuum ? configParams.autovacuum.toLowerCase() : 'incremental'; - obj.sqliteConfig.incrementalVacuum = configParams.incrementalvacuum ? configParams.incrementalvacuum : 100; - obj.sqliteConfig.journalMode = configParams.journalmode ? configParams.journalmode.toLowerCase() : 'delete'; + let configParams = parent.config.settings.sqlite3 || {}; + databaseName = (typeof configParams == 'string') ? configParams : (configParams.name || 'meshcentral'); + obj.sqliteConfig.startupVacuum = (typeof configParams.startupvacuum == 'boolean') ? configParams.startupvacuum : false; + obj.sqliteConfig.autoVacuum = (typeof configParams.autovacuum == 'string') ? configParams.autovacuum.toLowerCase() : 'incremental'; + if (!(['none','full','incremental'].includes(obj.sqliteConfig.autoVacuum))) { obj.sqliteConfig.autoVacuum = 'incremental'; } + const incrementalVacuum = Number(configParams.incrementalvacuum); + obj.sqliteConfig.incrementalVacuum = (Number.isInteger(incrementalVacuum) && incrementalVacuum >= 0) ? incrementalVacuum : 100; + //allowed modes, 'none' excluded because not usefull for this app, maybe also remove 'memory'? - if (!(['delete', 'truncate', 'persist', 'memory', 'wal'].includes(obj.sqliteConfig.journalMode))) { obj.sqliteConfig.journalMode = 'delete'}; - obj.sqliteConfig.journalSize = configParams.journalsize ? configParams.journalsize : 409600; + obj.sqliteConfig.journalMode = (typeof configParams.journalmode == 'string' && ['delete', 'truncate', 'persist', 'memory', 'wal'].includes(configParams.journalmode.toLowerCase())) ? configParams.journalmode.toLowerCase() : 'delete'; + const journalSize = Number(configParams.journalsize); + obj.sqliteConfig.journalSize = Number.isInteger(journalSize) ? journalSize : 4096000; + //wal can use the more performant 'normal' mode, see https://www.sqlite.org/pragma.html#pragma_synchronous obj.sqliteConfig.synchronous = (obj.sqliteConfig.journalMode == 'wal') ? 'normal' : 'full'; - if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'}; - if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'}; - obj.sqliteConfig.maintenance += 'PRAGMA optimize;'; - + if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'} + if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'} + parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4)); - if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') }; + if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') } //.cached not usefull obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) { if (err && (err.code == 'SQLITE_CANTOPEN')) { @@ -829,24 +833,20 @@ module.exports.CreateDB = function (parent, func) { CREATE INDEX ndxsmbiosexpire ON smbios (expire); `, function (err) { // Completed DB creation of SQLite3 - sqliteSetOptions(func); - //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity - setupFunctions(func); + sqliteSetOptions(function () { setupFunctions(func); }); } ); }); return; - } else if (err) { console.log("SQLite Error: " + err); process.exit(0); } + } else if (err) { console.log("SQLite Error: " + err); process.exit(1); } //for existing db's - sqliteSetOptions(); // Create any missing tables (e.g., pluginpermissions added in updates) obj.file.exec(` CREATE TABLE IF NOT EXISTS pluginpermissions (id VARCHAR(255) PRIMARY KEY, doc JSON) `, function (err) { if (err) { console.log("SQLite Error creating pluginpermissions table: " + err); } - //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity - setupFunctions(func); + sqliteSetOptions(function () { setupFunctions(func); }); }); }); } else if (parent.args.acebase) { @@ -1360,41 +1360,42 @@ module.exports.CreateDB = function (parent, func) { } function sqliteSetOptions(func) { - //get current auto_vacuum mode for comparison + // Read current auto_vacuum mode; switching into or out of 'none' requires a VACUUM to take effect. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum obj.file.get('PRAGMA auto_vacuum;', function(err, current){ - let pragma = 'PRAGMA journal_mode=' + obj.sqliteConfig.journalMode + ';' + - 'PRAGMA synchronous='+ obj.sqliteConfig.synchronous + ';' + - 'PRAGMA journal_size_limit=' + obj.sqliteConfig.journalSize + ';' + - 'PRAGMA auto_vacuum=' + obj.sqliteConfig.autoVacuum + ';' + - 'PRAGMA incremental_vacuum=' + obj.sqliteConfig.incrementalVacuum + ';' + - 'PRAGMA optimize=0x10002;'; - //check new autovacuum mode, if changing from or to 'none', a VACUUM needs to be done to activate it. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum - if ( obj.sqliteConfig.startupVacuum - || (current.auto_vacuum == 0 && obj.sqliteConfig.autoVacuum !='none') - || (current.auto_vacuum != 0 && obj.sqliteConfig.autoVacuum =='none')) - { - pragma += 'VACUUM;'; - }; + if (err || !current) { parent.debug('db', 'Could not read auto_vacuum: ' + (err ? err.message : 'no row')); current = { auto_vacuum: 0 }; } + let pragma = `PRAGMA journal_mode=${obj.sqliteConfig.journalMode}` + + `;PRAGMA synchronous=${obj.sqliteConfig.synchronous}` + + `;PRAGMA journal_size_limit=${obj.sqliteConfig.journalSize}` + + `;PRAGMA auto_vacuum=${obj.sqliteConfig.autoVacuum}` + + `;PRAGMA incremental_vacuum(${obj.sqliteConfig.incrementalVacuum})` + + `;PRAGMA optimize=0x10002;PRAGMA foreign_keys = ON;`; + // VACUUM needed only when crossing the 'none' boundary in either direction + if (obj.sqliteConfig.startupVacuum || ((current.auto_vacuum == 0) != (obj.sqliteConfig.autoVacuum == 'none'))) { pragma += 'VACUUM;'; } parent.debug ('db', 'Config statement: ' + pragma); - + obj.file.exec( pragma, function (err) { if (err) { parent.debug('db', 'Config pragma error: ' + (err.message)) }; - sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) { - parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue); - }); + // check if debug db is enabled to prevent unneccessary calls + if ((parent.debugSources != null) && ((parent.debugSources == '*') || (parent.debugSources.indexOf('db') >= 0))) { + sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) { + parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue); + }); + } + if (func) { func(); } }); }); - //setupFunctions(func); } function sqliteGetPragmas (pragmas, func){ - //pragmas can only be gotting one by one + //pragmas can only be gotten one by one pragmas.forEach (function (pragma) { obj.file.get('PRAGMA ' + pragma + ';', function(err, res){ - if (pragma == 'auto_vacuum') { res[pragma] = SQLITE_AUTOVACUUM[res[pragma]] }; - if (pragma == 'synchronous') { res[pragma] = SQLITE_SYNCHRONOUS[res[pragma]] }; - if (func) { func (pragma, res[pragma]); } + if (err || !res) { return; } + let value = res[pragma]; + if (pragma == 'auto_vacuum') { value = SQLITE_AUTOVACUUM[value]; } + else if (pragma == 'synchronous') { value = SQLITE_SYNCHRONOUS[value]; } + if (func) { func (pragma, value); } }); }); } @@ -1618,18 +1619,18 @@ module.exports.CreateDB = function (parent, func) { obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { if (limit == 0) { limit = -1; } // In SQLite, no limit is -1 if (id && (id != '')) { - sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) { + sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) { if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } } func(err, performTypedRecordDecrypt(docs)); }); } else { if (extrasids == null) { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) { + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) { if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } } func(err, performTypedRecordDecrypt(docs)); }); } else { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) { + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) { if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } } func(err, performTypedRecordDecrypt(docs)); }); @@ -1877,9 +1878,6 @@ module.exports.CreateDB = function (parent, func) { // Write a configuration file to the database obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); } - // List all configuration files - obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); } - // Get database information (TODO: Complete this) obj.getDbStats = function (func) { obj.stats = { c: 4 }; @@ -1934,7 +1932,7 @@ module.exports.CreateDB = function (parent, func) { } obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { if (meshes.length == 0) { func(null, []); return; } - var query = obj.file.query('meshcentral').skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain); + var query = obj.file.query('meshcentral').sort('name', true).skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain); if (id) { query = query.filter('_id', '==', id); } if (extrasids == null) { query = query.filter('meshid', 'in', meshes); @@ -2160,13 +2158,6 @@ module.exports.CreateDB = function (parent, func) { // Write a configuration file to the database obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); } - // List all configuration files - obj.listConfigFiles = function (func) { - obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) { - const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); - }); - } - // Get database information obj.getDbStats = function (func) { obj.stats = { c: 5 }; @@ -2220,12 +2211,12 @@ module.exports.CreateDB = function (parent, func) { obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) { if (limit == 0) { limit = 0xFFFFFFFF; } if (id && (id != '')) { - sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) ORDER BY LOWER(doc->>\'name\') LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } else { if (extrasids == null) { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true); + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) ORDER BY LOWER(doc->>\'name\') LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true); } else { - sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) ORDER BY LOWER(doc->>\'name\') LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } } }; @@ -2433,9 +2424,6 @@ module.exports.CreateDB = function (parent, func) { // Write a configuration file to the database obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); } - // List all configuration files - obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); } - // Get database information (TODO: Complete this) obj.getDbStats = function (func) { obj.stats = { c: 4 }; @@ -2484,9 +2472,9 @@ module.exports.CreateDB = function (parent, func) { if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead. if (id && (id != '')) { - sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) ORDER BY LOWER(JSON_UNQUOTE(JSON_EXTRACT(doc, \'$.name\'))) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } else { - sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); + sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) ORDER BY LOWER(JSON_UNQUOTE(JSON_EXTRACT(doc, \'$.name\'))) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); } }; obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) { @@ -2685,9 +2673,6 @@ module.exports.CreateDB = function (parent, func) { // Write a configuration file to the database obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); } - // List all configuration files - obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); } - // Get database information (TODO: Complete this) obj.getDbStats = function (func) { obj.stats = { c: 4 }; @@ -2789,14 +2774,14 @@ module.exports.CreateDB = function (parent, func) { if (extrasids == null) { const x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } - var f = obj.file.find(x, { type: 0 }); + var f = obj.file.find(x, { type: 0 }).collation({ locale: 'en', strength: 2 }).sort({ name: 1 }); if (skip > 0) f = f.skip(skip); // Skip records if (limit > 0) f = f.limit(limit); // Limit records f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } else { const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] }; if (id) { x._id = id; } - var f = obj.file.find(x, { type: 0 }); + var f = obj.file.find(x, { type: 0 }).collation({ locale: 'en', strength: 2 }).sort({ name: 1 }); if (skip > 0) f = f.skip(skip); // Skip records if (limit > 0) f = f.limit(limit); // Limit records f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); @@ -2987,9 +2972,6 @@ module.exports.CreateDB = function (parent, func) { // Write a configuration file to the database obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); } - // List all configuration files - obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); } - // Get database information obj.getDbStats = function (func) { obj.stats = { c: 6 }; @@ -3076,11 +3058,11 @@ module.exports.CreateDB = function (parent, func) { if (extrasids == null) { const x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } - obj.file.find(x).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); + obj.file.find(x).sort({ name: 1 }).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } else { const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] }; if (id) { x._id = id; } - obj.file.find(x).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); + obj.file.find(x).sort({ name: 1 }).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); } }; obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) { @@ -3206,9 +3188,6 @@ module.exports.CreateDB = function (parent, func) { // Write a configuration file to the database obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); } - // List all configuration files - obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); } - // Get database information obj.getDbStats = function (func) { obj.stats = { c: 5 }; @@ -3463,6 +3442,13 @@ module.exports.CreateDB = function (parent, func) { child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) { if ((error != null) && (error != '')) { func(1, "Mongodump error, backup will not be performed. Check path or use mongodumppath & mongodumpargs"); + + let processedError = error.message; + if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) { + processedError = processedError.replaceAll(encodeURIComponent(parent.config.settings.postgres.password), "****"); + } + parent.debug('backup', 'MongoDB/MongoJS DumpTool: ' + processedError); + return; } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;} }); @@ -3474,6 +3460,13 @@ module.exports.CreateDB = function (parent, func) { child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) { if ((error != null) && (error != '')) { func(1, "mysqldump error, backup will not be performed. Check path or use mysqldumppath"); + + let processedError = error.message; + if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) { + processedError = processedError.replaceAll(encodeURIComponent(parent.config.settings.postgres.password), "****"); + } + parent.debug('backup', 'MariaDB/MySQL DumpTool: ' + processedError); + return; } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;} @@ -3489,6 +3482,13 @@ module.exports.CreateDB = function (parent, func) { child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) { if ((error != null) && (error != '')) { func(1, "pg_dump error, backup will not be performed. Check path or use pgdumppath."); + + let processedError = error.message; + if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) { + processedError = processedError.replaceAll(encodeURIComponent(parent.config.settings.postgres.password), "****"); + } + parent.debug('backup', 'PostgreSQL DumpTool: ' + processedError); + return; } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;} }); @@ -3610,8 +3610,8 @@ module.exports.CreateDB = function (parent, func) { obj.performBackup = function (func) { parent.debug('backup','Entering performBackup'); try { - if (obj.performingBackup) return 'Backup alreay in progress.'; - if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }}; + if (obj.performingBackup) { return 'Backup already in progress.' }; + if (parent.config.settings.autobackup.backupintervalhours == -1) { return 'Backup disabled.' }; obj.performingBackup = true; let backupPath = parent.backuppath; let dataPath = parent.datapath; @@ -3707,12 +3707,13 @@ module.exports.CreateDB = function (parent, func) { let archive = null; let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9); - //if password defined, create encrypted zip - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) { + //if password defined, or a password entered for the manual backup, create encrypted zip + if ((parent.config.settings.autobackup.zippasswordrequest != '') && ((typeof parent.config.settings.autobackup.zippassword == 'string') || (typeof parent.config.settings.autobackup.zippasswordrequest == 'string'))) { try { //Only register format once, otherwise it triggers an error if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); } - archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword }); + archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', + password: (typeof parent.config.settings.autobackup.zippasswordrequest == 'string')?parent.config.settings.autobackup.zippasswordrequest:parent.config.settings.autobackup.zippassword }); if (func) { func('Creating encrypted ZIP'); } } catch (ex) { // registering encryption failed, do not fall back to non-encrypted, fail backup and skip old backup removal as a precaution to not lose any backups obj.backupStatus |= BACKUPFAIL_ZIPMODULE; @@ -3723,6 +3724,7 @@ module.exports.CreateDB = function (parent, func) { if (func) { func('Creating a NON-ENCRYPTED ZIP'); } archive = archiver('zip', { zlib: { level: zipLevel } }); } + delete parent.config.settings.autobackup.zippasswordrequest; //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP) if (obj.backupStatus == 0) { diff --git a/dependencies.txt b/dependencies.txt index dfc26a2780..8f54b4bfda 100644 --- a/dependencies.txt +++ b/dependencies.txt @@ -1,18 +1,18 @@ "@seald-io/nedb": "4.1.2", + "@zip.js/zip.js": "2.8.26", "archiver": "7.0.1", - "body-parser": "1.20.4", "cbor": "5.2.0", "compression": "1.8.1", "cookie-session": "2.1.1", - "express": "4.22.1", + "express": "4.22.2", "express-handlebars": "7.1.3", "express-ws": "5.0.2", "ipcheck": "0.1.0", "minimist": "1.2.8", - "multiparty": "4.2.3", - "node-forge": "1.3.2", - "otplib": "12.0.1", + "multiparty": "4.3.0", + "node-forge": "1.4.0", + "otplib": "13.4.1", "ua-client-hints-js": "0.1.2", "ua-parser-js": "1.0.40", - "ws": "8.18.3", + "ws": "8.21.0", "yauzl": "2.10.0" \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 554bce379e..1ce402df52 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,10 @@ +FROM alpine:3.24 AS node-base + +RUN apk add --no-cache --update \ + nodejs npm jq tzdata + ### STAGE 1 BUILDING. -FROM alpine:3.23 AS builder +FROM node-base AS mesh-compiler # Any value inside one of the disable ARGs will be accepted. ARG DISABLE_EXTRACT="yes" @@ -8,9 +13,7 @@ ARG DISABLE_TRANSLATE="yes" # NODE_OPTIONS="--max_old_space_size=4096" # If your process gets OOM killed, perhaps the above will help. -RUN apk add --no-cache --update \ - nodejs npm; \ - mkdir -p /opt/meshcentral/meshcentral +RUN mkdir -p /opt/meshcentral/meshcentral WORKDIR /opt/meshcentral COPY ./ /opt/meshcentral/meshcentral/ @@ -49,13 +52,13 @@ RUN rm -rf /opt/meshcentral/meshcentral/docker /opt/meshcentral/meshcentral/node ### STAGE 2 PRECOMPILE DEPS MODULE -FROM alpine:3.23 AS dep-compiler +FROM node-base AS dep-compiler RUN echo -e "----------\nINSTALLING ALPINE PACKAGES...\n----------"; \ apk add --no-cache --update \ - bash gcc g++ jq make nodejs npm python3 tzdata + bash gcc g++ make python3 -COPY --from=builder /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral +COPY --from=mesh-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral WORKDIR /opt/meshcentral/meshcentral RUN jq '.dependencies += {"modern-syslog": "1.2.0", "telegram": "2.26.22"}' package.json > temp.json && mv temp.json package.json \ @@ -65,7 +68,7 @@ RUN jq '.dependencies += {"modern-syslog": "1.2.0", "telegram": "2.26.22"}' pack ### STAGE 3 BUILDING. -FROM alpine:3.23 AS finalizer +FROM node-base AS finalizer # copy files from previous layer COPY --from=dep-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral @@ -131,15 +134,14 @@ ENV MARIADB_HOST="" \ RUN echo -e "----------\nINSTALLING ALPINE PACKAGES...\n----------"; \ mkdir -p /opt/meshcentral/meshcentral; \ apk add --no-cache --update \ - bash curl jq nodejs npm tzdata && \ + bash curl && \ rm -rf /var/cache/* \ /tmp/* \ /usr/share/man/ \ /usr/share/doc/ \ /var/log/* \ /var/spool/* \ - /usr/lib/debug/ && \ - npm install -g npm@latest + /usr/lib/debug/ WORKDIR /opt/meshcentral @@ -147,7 +149,7 @@ RUN case "$PREINSTALL_LIBS" in \ true|yes|TRUE|YES) \ cd meshcentral && \ echo -e "----------\nPREINSTALLING LIBRARIES...\n----------"; \ - npm install ssh2@1.16.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 otplib@12.0.1 yub@0.11.1;; \ + npm install ssh2@1.17.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 yub@0.11.1;; \ false|no|FALSE|NO) \ echo "Not pre-installing libraries.";; \ *) \ @@ -174,7 +176,7 @@ RUN case "$INCLUDE_POSTGRESQL_TOOLS" in \ apk add --no-cache postgresql-client && \ cd meshcentral && \ echo -e "----------\nPREINSTALLING POSTGRESQL LIBRARIES...\n----------"; \ - npm install pg@8.14.1;; \ + npm install pg@8.16.3;; \ false|no|FALSE|NO) \ echo "Not including PostgreSQL Tools.";; \ *) \ @@ -187,7 +189,7 @@ RUN case "$INCLUDE_MARIADB_TOOLS" in \ apk add --no-cache mariadb-client && \ cd meshcentral && \ echo -e "----------\nPREINSTALLING MARIADB/MYSQL LIBRARIES...\n----------"; \ - npm install mariadb@3.4.0 mysql2@3.11.4;; \ + npm install mariadb@3.4.5 mysql2@3.15.1;; \ false|no|FALSE|NO) \ echo "Not including MariaDB/MySQL Tools.";; \ *) \ diff --git a/docker/Dockerfile-debian b/docker/Dockerfile-debian index 6382ef3c44..a88925ed49 100644 --- a/docker/Dockerfile-debian +++ b/docker/Dockerfile-debian @@ -1,5 +1,22 @@ -### STAGE 1 BUILDING. -FROM debian:trixie-slim AS builder +### STAGE 1 NODEJS BASE IMAGE + +FROM debian:trixie-slim AS node-base + +ARG NODE_MAJOR=24 + +# Install general dependencies and nodesource's NodeJS +RUN apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests \ + ca-certificates curl gnupg2 jq tzdata wget && \ + mkdir -p /etc/apt/keyrings/ && \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ + echo "Types: deb \nURIs: https://deb.nodesource.com/node_${NODE_MAJOR}.x/ \nSuites: nodistro \nComponents: main \nSigned-By: /etc/apt/keyrings/nodesource.gpg" > /etc/apt/sources.list.d/nodesource.sources && cat /etc/apt/sources.list.d/nodesource.sources && \ + apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests \ + nodejs + +### STAGE 2 BUILDING MESHCENTRAL BASICS +FROM node-base AS mesh-compiler # Any value inside one of the disable ARGs will be accepted. ARG DISABLE_EXTRACT="yes" @@ -8,39 +25,34 @@ ARG DISABLE_TRANSLATE="yes" # NODE_OPTIONS="--max_old_space_size=4096" # If your process gets OOM killed, perhaps the above will help. -# Install nodesource's NodeJS -RUN apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests \ - nodejs npm && \ - npm install -g npm@latest && \ - mkdir -p /opt/meshcentral/meshcentral +RUN mkdir -p /opt/meshcentral/meshcentral WORKDIR /opt/meshcentral COPY ./ /opt/meshcentral/meshcentral/ # Check the Docker build arguments and if they are empty do the task. RUN if [ -n "$DISABLE_EXTRACT" ] || [ -n "$DISABLE_MINIFY" ] || [ -n "$DISABLE_TRANSLATE" ]; then \ - echo -e "----------\nPREPARING ENVIRONMENT...\n----------"; \ + echo "----------\nPREPARING ENVIRONMENT...\n----------"; \ cd meshcentral && \ npm install html-minifier-terser@7.2.0 jsdom@26.0.0 esprima@4.0.1 && \ cd translate && \ case "$DISABLE_EXTRACT" in \ false|no|FALSE|NO) \ - echo -e "----------\nSTARTING THE EXTRACTING PROCESS...\n----------"; \ + echo "----------\nSTARTING THE EXTRACTING PROCESS...\n----------"; \ node translate.js extractall;; \ *) \ echo "Setting EXTRACT as disabled.";; \ esac && \ case "$DISABLE_MINIFY" in \ false|no|FALSE|NO) \ - echo -e "----------\nSTARTING THE MINIFYING PROCESS...\n----------"; \ + echo "----------\nSTARTING THE MINIFYING PROCESS...\n----------"; \ node translate.js minifyall;; \ *) \ echo "Setting MINIFY as disabled.";; \ esac && \ case "$DISABLE_TRANSLATE" in \ false|no|FALSE|NO) \ - echo -e "----------\nSTARTING THE TRANSLATING PROCESS...\n----------"; \ + echo "----------\nSTARTING THE TRANSLATING PROCESS...\n----------"; \ node translate.js translateall;; \ *) \ echo "Setting TRANSLATE as disabled.";; \ @@ -51,30 +63,27 @@ RUN if [ -n "$DISABLE_EXTRACT" ] || [ -n "$DISABLE_MINIFY" ] || [ -n "$DISABLE_T RUN rm -rf /opt/meshcentral/meshcentral/docker /opt/meshcentral/meshcentral/node_modules /opt/meshcentral/meshcentral/docs -### STAGE 2 PRECOMPILE DEPS MODULE +### STAGE 3 PRECOMPILE NODEJS DEPENDENCIES -FROM debian:trixie-slim AS dep-compiler +FROM node-base AS dep-compiler ENV NODE_ENV="production" -# Install general dependencies and nodesource's NodeJS RUN apt-get update && \ - echo -e "----------\nINSTALLING DEBIAN PACKAGES...\n----------"; \ apt-get install -y --no-install-recommends --no-install-suggests \ - bash gcc g++ jq make nodejs npm python3 tzdata && \ - npm install -g npm@latest + gcc g++ make python3 -COPY --from=builder /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral +COPY --from=mesh-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral WORKDIR /opt/meshcentral/meshcentral RUN jq '.dependencies += {"modern-syslog": "1.2.0", "telegram": "2.26.22"}' package.json > temp.json && mv temp.json package.json \ - && npm i --package-lock-only \ + && npm install --package-lock-only \ && npm ci --omit=dev \ && npm cache clean --force -### STAGE 3 fun. building from source... +### STAGE 4 COMPILING MONGODB TOOLS AND DEPENDENCIES -FROM golang:trixie AS mongo-tools-compiler +FROM golang:trixie AS mongo-compiler ARG INCLUDE_MONGODB_TOOLS="false" @@ -95,15 +104,13 @@ RUN case "$INCLUDE_MONGODB_TOOLS" in \ exit 1;; \ esac -### STAGE 4 BUILDING. +### STAGE 5 COMPILING LAYERS AND FORGING MESHCENTRAL STATE -FROM debian:trixie-slim AS finalizer - -ARG NODEJS_DOWNLOAD="https://nodejs.org/dist/v24.12.0/node-v24.12.0-linux-x64.tar.xz" +FROM node-base AS finalizer # Copy files from previous layers COPY --from=dep-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral -COPY --from=mongo-tools-compiler /mongo-tools/bin/ /tmp/bin/ +COPY --from=mongo-compiler /mongo-tools/bin/ /tmp/bin/ # environment variables ENV NODE_ENV="production" \ @@ -165,12 +172,7 @@ ENV MARIADB_HOST="" \ WORKDIR /opt/meshcentral -RUN apt-get update && \ - echo -e "----------\nINSTALLING DEBIAN PACKAGES...\n----------"; \ - apt-get install -y --no-install-recommends --no-install-suggests \ - bash ca-certificates curl jq nodejs npm tzdata && \ - npm install -g npm@latest && \ - rm -rfv \ +RUN rm -rfv \ /var/cache/* \ /usr/share/man/ \ /usr/share/doc/ \ @@ -182,12 +184,12 @@ RUN apt-get update && \ RUN case "$PREINSTALL_LIBS" in \ true|yes|TRUE|YES) \ cd meshcentral && \ - echo -e "----------\nPREINSTALLING LIBRARIES...\n----------"; \ - npm install ssh2@1.16.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 otplib@12.0.1 yub@0.11.1;; \ + echo "----------\nPREINSTALLING LIBRARIES...\n----------"; \ + npm install ssh2@1.17.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 yub@0.11.1;; \ false|no|FALSE|NO) \ echo "Not pre-installing libraries.";; \ *) \ - echo -e "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \ + echo "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \ exit 1;; \ esac @@ -196,7 +198,7 @@ RUN case "$INCLUDE_MONGODB_TOOLS" in \ true|yes|TRUE|YES) \ mv /tmp/bin/* /usr/bin; \ cd meshcentral && \ - echo -e "----------\nPREINSTALLING MONGODB LIBRARIES...\n----------"; \ + echo "----------\nPREINSTALLING MONGODB LIBRARIES...\n----------"; \ npm install mongodb@4.17.2 @mongodb-js/saslprep@1.3.1;; \ false|no|FALSE|NO) \ echo "Not including MongoDB Tools.";; \ @@ -209,12 +211,12 @@ RUN case "$INCLUDE_POSTGRESQL_TOOLS" in \ true|yes|TRUE|YES) \ apt-get install -y --no-install-recommends --no-install-suggests postgresql-client-17; \ cd meshcentral && \ - echo -e "----------\nPREINSTALLING POSTGRESQL LIBRARIES...\n----------"; \ - npm install pg@8.14.1;; \ + echo "----------\nPREINSTALLING POSTGRESQL LIBRARIES...\n----------"; \ + npm install pg@8.16.3;; \ false|no|FALSE|NO) \ echo "Not including PostgreSQL Tools.";; \ *) \ - echo -e "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \ + echo "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \ exit 1;; \ esac @@ -222,12 +224,12 @@ RUN case "$INCLUDE_MARIADB_TOOLS" in \ true|yes|TRUE|YES) \ apt-get install -y --no-install-recommends --no-install-suggests default-mysql-client mariadb-client; \ cd meshcentral && \ - echo -e "----------\nPREINSTALLING MARIADB/MYSQL LIBRARIES...\n----------"; \ - npm install mariadb@3.4.0 mysql2@3.11.4;; \ + echo "----------\nPREINSTALLING MARIADB/MYSQL LIBRARIES...\n----------"; \ + npm install mariadb@3.4.5 mysql2@3.15.1;; \ false|no|FALSE|NO) \ echo "Not including MariaDB/MySQL Tools.";; \ *) \ - echo -e "Invalid value for build argument INCLUDE_MARIADB_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \ + echo "Invalid value for build argument INCLUDE_MARIADB_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \ exit 1;; \ esac diff --git a/docker/README.md b/docker/README.md index be04ebd479..a072bbc370 100644 --- a/docker/README.md +++ b/docker/README.md @@ -61,6 +61,8 @@ Below is a breakdown of environment variables used in this setup. | REVERSE_PROXY | "" | Configures reverse proxy support through `certUrl`. | | REVERSE_PROXY_TLS_PORT | "443" | Configures reverse proxy TLS port, will be combined with: `REVERSE_PROXY`. | | WEBRTC | false | Enables/disables WebRTC support. | +| TRUSTED_PROXY | "" | Trust forwarded headers from these IPs or domains. | +| TLS_OFFLOAD | false | When set to true, indicate that TLS is being performed by a device in front of MeshCentral. | ### Database Configuration diff --git a/docker/compose.yaml b/docker/compose.yaml index d0745f0326..a2040cee14 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -15,8 +15,8 @@ services: - meshcentral-web:/opt/meshcentral/meshcentral-web - meshcentral-backups:/opt/meshcentral/meshcentral-backups ports: - - ${PORT}:${PORT} - - ${REDIR_PORT}:${REDIR_PORT} + - 443:443 + - 80:80 volumes: meshcentral-data: meshcentral-files: diff --git a/docker/config.json.template b/docker/config.json.template index 588d2184b6..dfff6bdab4 100644 --- a/docker/config.json.template +++ b/docker/config.json.template @@ -13,7 +13,8 @@ "redirPort": 80, "_redirAliasPort": 80, "AgentPong": 300, - "TLSOffload": false, + "tlsOffload": false, + "_trustedProxy": "", "SelfUpdate": false, "AllowFraming": false, "WebRTC": false, @@ -51,4 +52,4 @@ "_names": "myserver.mydomain.com", "production": false } -} \ No newline at end of file +} diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index f535b9d435..82afe304bc 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,12 +1,15 @@ #!/bin/bash +echo "NodeJS: $(node -v)" +echo "NPM: $(npm -v)" + # Origin: https://github.com/Melo-Professional/MeshCentral-Stylish-UI stylishui_base_url="https://github.com/Melo-Professional/MeshCentral-Stylish-UI/archive/refs" stylishui_compat="https://raw.githubusercontent.com/Melo-Professional/MeshCentral-Stylish-UI/refs/heads/main/metadata/compat.json" function graceful_shutdown() { echo "Received SIGTERM from the container host. Cleaning up..." - kill -SIGINT $meshcentral_pid + kill -SIGINT "$meshcentral_pid" echo "MeshCentral process stopped. Exiting..." exit 0 @@ -14,8 +17,7 @@ function graceful_shutdown() { trap graceful_shutdown SIGTERM function test_url() { - wget --spider $1 &> /dev/null - if [[ $? -eq 0 ]]; then + if wget --spider "$1" &> /dev/null; then echo "is ok." return 0 else @@ -24,10 +26,11 @@ function test_url() { fi } + function dynamic_config() { # BEGIN DATABASE CONFIGURATION FIELDS USE_MONGODB=${USE_MONGODB,,} - if [[ $USE_MONGODB =~ ^(true|yes)$ ]]; then + if [[ "$USE_MONGODB" =~ ^(true|yes)$ ]]; then echo "Enabling MongoDB-connector..." if [[ -n "$MONGO_URL" ]]; then @@ -47,7 +50,7 @@ function dynamic_config() { fi USE_POSTGRESQL=${USE_POSTGRESQL,,} - if [[ $USE_POSTGRESQL =~ ^(true|yes)$ ]]; then + if [[ "$USE_POSTGRESQL" =~ ^(true|yes)$ ]]; then echo "Enabling PostgreSQL-connector..." sed -i 's/"_postgres"/"postgres"/' "$CONFIG_FILE" @@ -68,7 +71,7 @@ function dynamic_config() { fi USE_MARIADB=${USE_MARIADB,,} - if [[ $USE_MARIADB =~ ^(true|yes)$ ]]; then + if [[ "$USE_MARIADB" =~ ^(true|yes)$ ]]; then echo "Enabling MariaDB-connector..." sed -i 's/"_mariaDB"/"mariaDB"/' "$CONFIG_FILE" jq --arg mariadb_host "$MARIADB_HOST" \ @@ -96,7 +99,7 @@ function dynamic_config() { echo "If defaults are going to get applied, refer to: https://raw.githubusercontent.com/Ylianst/MeshCentral/master/meshcentral-config-schema.json" # SESSIONKEY - if [[ ${REGEN_SESSIONKEY,,} =~ ^(true|yes)$ ]]; then + if [[ "${REGEN_SESSIONKEY,,}" =~ ^(true|yes)$ ]]; then echo "Regenerating Session-Key because REGENSESSIONKEY is 'true' or 'yes'" SESSION_KEY=$(tr -dc 'A-Z0-9' < /dev/urandom | fold -w 96 | head -n 1) @@ -109,50 +112,50 @@ function dynamic_config() { fi # HOSTNAME - if [[ -n $HOSTNAME ]]; then + if [[ -n "$HOSTNAME" ]]; then echo "Setting hostname (cert)... $HOSTNAME" jq --arg hostname "$HOSTNAME" \ '.settings.cert = $hostname' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" else - echo "Invalid or no hostname, defaulting to 'localhost', value given: $HOSTNAME" + echo "Invalid or no hostname, defaulting to 'localhost', value(s) given: ${HOSTNAME:-empty}" jq --arg hostname "localhost" \ '.settings.cert = $hostname' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" fi # PORT - if [[ -n $PORT ]]; then + if [[ -n "$PORT" ]]; then echo "Setting port... $PORT" jq --arg port "$PORT" \ - '.settings.port = $port' \ + '.settings.port = ($port|tonumber)' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" else - echo "Invalid or no port, defaulting to '443', value given: $PORT" + echo "Invalid or no port, defaulting to '443', Value(s) given: ${PORT:-empty}" jq --arg port "443" \ - '.settings.port = $port' \ + '.settings.port = ($port|tonumber)' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" fi # REDIR_PORT - if [[ -n $REDIR_PORT ]]; then + if [[ -n "$REDIR_PORT" ]]; then echo "Setting redirport... $REDIR_PORT" jq --arg redirport "$REDIR_PORT" \ - '.settings.redirPort = $redirport' \ + '.settings.redirPort = ($redirport|tonumber)' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" else - echo "Invalid or no redirport, defaulting to '80', value given: $REDIR_PORT" + echo "Invalid or no redirport, defaulting to '80', Value(s) given: ${REDIR_PORT:-empty}" jq --arg redirport "80" \ - '.settings.redirPort = $redirport' \ + '.settings.redirPort = ($redirport|tonumber)' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" fi # ALLOWPLUGINS ALLOW_PLUGINS=${ALLOW_PLUGINS,,} - if [[ $ALLOW_PLUGINS =~ ^(true|false)$ ]]; then + if [[ "$ALLOW_PLUGINS" =~ ^(true|false)$ ]]; then echo "Setting plugins... $ALLOW_PLUGINS" sed -i 's/"_plugins"/"plugins"/' "$CONFIG_FILE" @@ -160,13 +163,13 @@ function dynamic_config() { '.settings.plugins.enabled = $allow_plugins' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" else - echo "Invalid or no ALLOWPLUGINS value given, commenting out so default applies... Value given: $ALLOW_PLUGINS" + echo "Invalid or no ALLOWPLUGINS value given, commenting out so default applies... Value(s) given: ${ALLOW_PLUGINS:-empty}" sed -i 's/"plugins":/"_plugins":/g' "$CONFIG_FILE" fi # WEBRTC WEBRTC=${WEBRTC,,} - if [[ $WEBRTC =~ ^(true|false)$ ]]; then + if [[ "$WEBRTC" =~ ^(true|false)$ ]]; then echo "Setting WebRTC... $WEBRTC" sed -i 's/"_WebRTC"/"WebRTC"/' "$CONFIG_FILE" @@ -175,13 +178,13 @@ function dynamic_config() { "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" #sed -i "s/\"WebRTC\": *[a-z]*/\"WebRTC\": $WEBRTC/" "$CONFIG_FILE" else - echo "Invalid or no WEBRTC value given, commenting out so default applies... Value given: $WEBRTC" + echo "Invalid or no WEBRTC value given, commenting out so default applies... Value(s) given: ${WEBRTC:-empty}" sed -i 's/"WebRTC":/"_WebRTC":/g' "$CONFIG_FILE" fi # IFRAME IFRAME=${IFRAME,,} - if [[ $IFRAME =~ ^(true|false)$ ]]; then + if [[ "$IFRAME" =~ ^(true|false)$ ]]; then echo "Setting AllowFraming... $IFRAME" sed -i 's/"_AllowFraming"/"AllowFraming"/' "$CONFIG_FILE" @@ -189,15 +192,44 @@ function dynamic_config() { '.settings.AllowFraming = $allow_framing' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" else - echo "Invalid or no IFRAME value given, commenting out so default applies... Value given: $IFRAME" + echo "Invalid or no IFRAME value given, commenting out so default applies... Value(s) given: ${IFRAME:-empty}" sed -i 's/"AllowFraming":/"_AllowFraming":/g' "$CONFIG_FILE" fi + # ALLOWED_ORIGIN + ALLOWED_ORIGIN=${ALLOWED_ORIGIN,,} + if [[ "$ALLOWED_ORIGIN" =~ ^(true|false)$ ]]; then + echo "Setting allowedOrigin... $ALLOWED_ORIGIN" + + sed -i 's/"_allowedOrigin"/"allowedOrigin"/' "$CONFIG_FILE" + jq --argjson allowed_origin "$ALLOWED_ORIGIN" \ + '.domains[""].allowedOrigin = $allowed_origin' \ + "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" + else + echo "Invalid or no ALLOWED_ORIGIN value given, commenting out so default applies... Value(s) given: ${ALLOWED_ORIGIN:-empty}" + sed -i 's/"allowedOrigin":/"_allowedOrigin":/g' "$CONFIG_FILE" + fi + + # certUrl || reverseProxy + if [[ -n "$REVERSE_PROXY" ]] && [[ -n "$REVERSE_PROXY_TLS_PORT" ]]; then + REVERSE_PROXY_STRING="${REVERSE_PROXY}:${REVERSE_PROXY_TLS_PORT}" + + echo "Setting certUrl... - $REVERSE_PROXY_STRING" + sed -i 's/"_certUrl"/"certUrl"/' "$CONFIG_FILE" + jq --arg cert_url "$REVERSE_PROXY_STRING" \ + '.domains[""].certUrl = $cert_url' \ + "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" + #sed -i "s/\"certUrl\": *[a-z]*/\"certUrl\": $REVERSE_PROXY_STRING/" "$CONFIG_FILE" + else + echo "Invalid or no REVERSE_PROXY and/or REVERSE_PROXY_TLS_PORT value given, commenting out so default applies... Value(s) given: ${REVERSE_PROXY_STRING:-empty}" + sed -i 's/"certUrl":/"_certUrl":/g' "$CONFIG_FILE" + fi + # trustedProxy - if [[ -n $TRUSTED_PROXY ]]; then + if [[ -n "$TRUSTED_PROXY" ]]; then echo "Setting trustedProxy... - $TRUSTED_PROXY" - if [[ $TRUSTED_PROXY == "all" ]] || [[ $TRUSTED_PROXY == "true" ]]; then + if [[ "$TRUSTED_PROXY" == "all" ]] || [[ "$TRUSTED_PROXY" == "true" ]]; then sed -i 's/"_trustedProxy"/"trustedProxy"/' "$CONFIG_FILE" jq --argjson trusted_proxy "true" \ '.settings.trustedProxy = $trusted_proxy' \ @@ -209,13 +241,26 @@ function dynamic_config() { "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" fi else - echo "Invalid or no REVERSE_PROXY and/or REVERSE_PROXY_TLS_PORT value given, commenting out so default applies... Value(s) given: $REVERSE_PROXY_STRING" - sed -i 's/"certUrl":/"_certUrl":/g' "$CONFIG_FILE" + echo "Invalid or no TRUSTED_PROXY value given, commenting out so default applies... Value(s) given: ${TRUSTED_PROXY:-empty}" + sed -i 's/"trustedProxy":/"_trustedProxy":/g' "$CONFIG_FILE" + fi + + # tlsOffload + if [[ -n "$TLS_OFFLOAD" ]]; then + echo "Setting tlsOffload... - $TLS_OFFLOAD" + + sed -i 's/"_tlsOffload"/"tlsOffload"/' "$CONFIG_FILE" + jq --arg tls_offload "$TLS_OFFLOAD" \ + '.settings.tlsOffload = $tls_offload' \ + "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" + else + echo "Invalid or no TLS_OFFLOAD value given, commenting out so default applies... Value(s) given: ${TLS_OFFLOAD:-empty}" + sed -i 's/"tlsOffload":/"_tlsOffload":/g' "$CONFIG_FILE" fi # ALLOW_NEW_ACCOUNTS ALLOW_NEW_ACCOUNTS=${ALLOW_NEW_ACCOUNTS,,} - if [[ $ALLOW_NEW_ACCOUNTS =~ ^(true|false)$ ]]; then + if [[ "$ALLOW_NEW_ACCOUNTS" =~ ^(true|false)$ ]]; then echo "Setting NewAccounts... $ALLOW_NEW_ACCOUNTS" sed -i 's/"_NewAccounts"/"NewAccounts"/' "$CONFIG_FILE" @@ -223,13 +268,13 @@ function dynamic_config() { '.domains[""].NewAccounts = $new_accounts' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" else - echo "Invalid or no ALLOW_NEW_ACCOUNTS value given, commenting out so default applies... Value given: $ALLOW_NEW_ACCOUNTS" + echo "Invalid or no ALLOW_NEW_ACCOUNTS value given, commenting out so default applies... Value(s) given: ${ALLOW_NEW_ACCOUNTS:-empty}" sed -i 's/"NewAccounts":/"_NewAccounts":/g' "$CONFIG_FILE" fi # LOCALSESSIONRECORDING LOCAL_SESSION_RECORDING=${LOCAL_SESSION_RECORDING,,} - if [[ $LOCAL_SESSION_RECORDING =~ ^(true|false)$ ]]; then + if [[ "$LOCAL_SESSION_RECORDING" =~ ^(true|false)$ ]]; then echo "Setting localSessionRecording... $LOCAL_SESSION_RECORDING" sed -i 's/"_localSessionRecording"/"localSessionRecording"/' "$CONFIG_FILE" @@ -237,13 +282,13 @@ function dynamic_config() { '.domains[""].localSessionRecording = $session_recording' \ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" else - echo "Invalid or no LOCALSESSIONRECORDING value given, commenting out so default applies... Value given: $LOCAL_SESSION_RECORDING" + echo "Invalid or no LOCALSESSIONRECORDING value given, commenting out so default applies... Value(s) given: ${LOCAL_SESSION_RECORDING:-empty}" sed -i 's/"localSessionRecording":/"_localSessionRecording":/g' "$CONFIG_FILE" fi # MINIFY MINIFY=${MINIFY,,} - if [[ $MINIFY =~ ^(true|false)$ ]]; then + if [[ "$MINIFY" =~ ^(true|false)$ ]]; then echo "Setting minify... $MINIFY" sed -i 's/"_minify"/"minify"/' "$CONFIG_FILE" @@ -252,46 +297,17 @@ function dynamic_config() { "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" #sed -i "s/\"minify\": *[a-z]*/\"minify\": $MINIFY/" "$CONFIG_FILE" else - echo "Invalid or no MINIFY value given, commenting out so default applies... Value given: $MINIFY" + echo "Invalid or no MINIFY value given, commenting out so default applies... Value(s) given: ${MINIFY:-empty}" sed -i 's/"minify":/"_minify":/g' "$CONFIG_FILE" fi - # ALLOWED_ORIGIN - ALLOWED_ORIGIN=${ALLOWED_ORIGIN,,} - if [[ $ALLOWED_ORIGIN =~ ^(true|false)$ ]]; then - echo "Setting allowedOrigin... $ALLOWED_ORIGIN" - - sed -i 's/"_allowedOrigin"/"allowedOrigin"/' "$CONFIG_FILE" - jq --argjson allowed_origin "$ALLOWED_ORIGIN" \ - '.domains[""].allowedOrigin = $allowed_origin' \ - "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" - else - echo "Invalid or no ALLOWED_ORIGIN value given, commenting out so default applies... Value given: $ALLOWED_ORIGIN" - sed -i 's/"allowedOrigin":/"_allowedOrigin":/g' "$CONFIG_FILE" - fi - - # certUrl - if [[ -n $REVERSE_PROXY ]] && [[ -n $REVERSE_PROXY_TLS_PORT ]]; then - REVERSE_PROXY_STRING="${REVERSE_PROXY}:${REVERSE_PROXY_TLS_PORT}" - - echo "Setting certUrl... - $REVERSE_PROXY_STRING" - sed -i 's/"_certUrl"/"certUrl"/' "$CONFIG_FILE" - jq --arg cert_url "$REVERSE_PROXY_STRING" \ - '.domains[""].certUrl = $cert_url' \ - "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE" - #sed -i "s/\"certUrl\": *[a-z]*/\"certUrl\": $REVERSE_PROXY_STRING/" "$CONFIG_FILE" - else - echo "Invalid or no REVERSE_PROXY and/or REVERSE_PROXY_TLS_PORT value given, commenting out so default applies... Value(s) given: $REVERSE_PROXY_STRING" - sed -i 's/"certUrl":/"_certUrl":/g' "$CONFIG_FILE" - fi - cat "$CONFIG_FILE" } function install_stylishui() { # Start by testing if we can determine compatibility printf "Testing compatibility schema URL..." - if ! test_url $stylishui_compat; then + if ! test_url "$stylishui_compat"; then echo "Compat URL failed." return 1 fi @@ -301,7 +317,7 @@ function install_stylishui() { full_url="${stylishui_base_url}/heads/main.tar.gz" else # Retrieve the values we need to determine compatibility - compat_data=$(curl -fsSL $stylishui_compat) + compat_data=$(curl -fsSL "$stylishui_compat") meshcentral_version=$(jq -r '.version' /opt/meshcentral/meshcentral/package.json) # Target the StylishUI version we need for our present Meshcentral version compat_version=$(echo "$compat_data" | jq -r --arg mcv "$meshcentral_version" \ @@ -314,18 +330,18 @@ function install_stylishui() { # Test if we can reach the data/content URL on github printf "Testing content URL..." - if ! test_url $full_url; then + if ! test_url "$full_url"; then echo "StylishUI URL failed." return 1 fi # Lets download and install the UI - wget -O /tmp/stylishui.tar.gz $full_url > /dev/null + wget -O /tmp/stylishui.tar.gz "$full_url" > /dev/null tar -xzf /tmp/stylishui.tar.gz -C /tmp web_folder=$(find /tmp -name meshcentral-web) # Check if we have some integrity - if [[ -z $web_folder ]]; then + if [[ -z "$web_folder" ]]; then echo "Installation failed, cleaning..." rm /tmp/stylishui* return 1 @@ -365,12 +381,12 @@ fi if [ -f "${CONFIG_FILE}" ]; then echo "Pre-existing config found, not recreating..." else - if [ ! -d $(dirname "$CONFIG_FILE") ]; then + if [ ! -d "$(dirname "$CONFIG_FILE")" ]; then echo "Creating meshcentral-data directory..." mkdir -p /opt/meshcentral/meshcentral-data fi - echo "Placing template into the relevant directory: $(dirname $CONFIG_FILE)" + echo "Placing template into the relevant directory: $(dirname "$CONFIG_FILE")" cp /opt/meshcentral/config.json.template "${CONFIG_FILE}" fi diff --git a/emails/translations/account-check_bs.html b/emails/translations/account-check_bs.html deleted file mode 100644 index b568f6e2b9..0000000000 --- a/emails/translations/account-check_bs.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Potvrda e-pošte
-
- - - - -
- [[[SERVERNAME]]] - Potvrda -
-

Zdravo [[[USERNAME]]], [[[SERVERNAME]]] traži verifikaciju e-pošte, kliknite na sljedeću vezu da dovršite proces.

-

- Kliknite ovdje da potvrdite svoju e-mail adresu. -

- Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku. -
\ No newline at end of file diff --git a/emails/translations/account-check_bs.txt b/emails/translations/account-check_bs.txt deleted file mode 100644 index 5b4ddb0fda..0000000000 --- a/emails/translations/account-check_bs.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Potvrda e-pošte -Zdravo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) vrši verifikaciju e-pošte. Idite na sljedeću vezu da dovršite proces: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku. \ No newline at end of file diff --git a/emails/translations/account-check_cs.html b/emails/translations/account-check_cs.html deleted file mode 100644 index 64112c51a1..0000000000 --- a/emails/translations/account-check_cs.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Ověření e-mailem
-
- - - - -
- [[[SERVERNAME]]] - Ověření -
-

Ahoj [[[USERNAME]]], [[[SERVERNAME]]] požaduje ověření e-mailem a dokončete proces kliknutím na následující odkaz.

-

- Kliknutím sem ověřte svou e-mailovou adresu. -

- Pokud jste tento požadavek nezačali, ignorujte tento e-mail. -
\ No newline at end of file diff --git a/emails/translations/account-check_cs.txt b/emails/translations/account-check_cs.txt deleted file mode 100644 index 6bf7299f7d..0000000000 --- a/emails/translations/account-check_cs.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Ověření e-mailem -Ahoj [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) provádí ověření e-mailem. Chcete-li proces dokončit, přejděte na následující odkaz: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Pokud jste tento požadavek nezačali, ignorujte tento e-mail. \ No newline at end of file diff --git a/emails/translations/account-check_da.html b/emails/translations/account-check_da.html deleted file mode 100644 index feb0437a0f..0000000000 --- a/emails/translations/account-check_da.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Email verificering
-
- - - - -
- [[[SERVERNAME]]] - Verificering -
-

Hej [[[USERNAME]], [[[SERVERNAME]]] anmoder om e-mailbekræftelse, klik på følgende link for at fuldføre processen.

-

- Klik her for at bekræfte din e-mailadresse. -

- Hvis du ikke startede denne anmodning, bedes du ignorere denne mail. -
\ No newline at end of file diff --git a/emails/translations/account-check_da.txt b/emails/translations/account-check_da.txt deleted file mode 100644 index 3ee23dc77e..0000000000 --- a/emails/translations/account-check_da.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Email verificering -Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) udfører en e-mailbekræftelse. Gå til følgende link for at fuldføre processen: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Hvis du ikke startede denne anmodning, bedes du ignorere denne mail. \ No newline at end of file diff --git a/emails/translations/account-check_de.html b/emails/translations/account-check_de.html deleted file mode 100644 index ec9a7d2e30..0000000000 --- a/emails/translations/account-check_de.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - E-Mail-Überprüfung
-
- - - - -
- [[[SERVERNAME]]] - Überprüfung -
-

Hallo [[[USERNAME]]], [[[SERVERNAME]]] Wenn Sie eine E-Mail-Bestätigung anfordern, klicken Sie auf den folgenden Link, um den Vorgang abzuschließen.

-

- Klicken Sie hier, um Ihre E-Mail-Adresse zu bestätigen. -

- Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte. -
\ No newline at end of file diff --git a/emails/translations/account-check_de.txt b/emails/translations/account-check_de.txt deleted file mode 100644 index df7bc55ffc..0000000000 --- a/emails/translations/account-check_de.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - E-Mail-Überprüfung -Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) führt eine E-Mail-Überprüfung durch. Klicken Sie auf den folgenden Link, um den Vorgang abzuschließen: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte. \ No newline at end of file diff --git a/emails/translations/account-check_es.html b/emails/translations/account-check_es.html deleted file mode 100644 index 709eba822a..0000000000 --- a/emails/translations/account-check_es.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Verificación de Correo Electrónico
-
- - - - -
- [[[SERVERNAME]]] - Verificación -
-

Hola [[[USERNAME]]], [[[SERVERNAME]]] esta solicitando la verificación por correo electrónico, haz clic en el siguiente enlace para completar el proceso.

-

- Haz clic aquí para verificar tu dirección de correo electrónico. -

- Si tu no iniciaste este requerimiento, por favor ignora este correo. -
\ No newline at end of file diff --git a/emails/translations/account-check_es.txt b/emails/translations/account-check_es.txt deleted file mode 100644 index 5be9acca5a..0000000000 --- a/emails/translations/account-check_es.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Verificación de Correo Electrónico -Hola [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) esta realizando una comprobación del correo electrónico. Navega al siguiente enlace para completar el proceso: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Si tu no iniciaste este requerimiento, por favor ignora este correo. \ No newline at end of file diff --git a/emails/translations/account-check_fi.html b/emails/translations/account-check_fi.html deleted file mode 100644 index 8f7caf7e88..0000000000 --- a/emails/translations/account-check_fi.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Sähköpostivarmistus
-
- - - - -
- [[[SERVERNAME]]] - Todentaminen -
-

Moi [[[USERNAME]]], [[[SERVERNAME]]] pyytää sähköpostitse vahvistusta, suorita prosessi napsauttamalla seuraavaa linkkiä.

-

- Napsauta tätä vahvistaaksesi sähköpostiosoitteesi. -

- Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin. -
\ No newline at end of file diff --git a/emails/translations/account-check_fi.txt b/emails/translations/account-check_fi.txt deleted file mode 100644 index bde9a7a515..0000000000 --- a/emails/translations/account-check_fi.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Sähköpostivarmistus -Hei [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) tarkistaa sähköpostiosoitetta. Seuraa linkkiä prosessin loppuun saattamiseksi: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin. \ No newline at end of file diff --git a/emails/translations/account-check_fr.html b/emails/translations/account-check_fr.html deleted file mode 100644 index 82d7beff04..0000000000 --- a/emails/translations/account-check_fr.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Vérification E-mail
-
- - - - -
- [[[SERVERNAME]]] - Vérification -
-

Bonjour [[[USERNAME]]], [[[SERVERNAME]]] demande une vérification par e-mail, cliquez sur le lien suivant pour terminer le processus.

-

- Cliquez ici pour vérifier votre adresse e-mail. -

- Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier. -
\ No newline at end of file diff --git a/emails/translations/account-check_fr.txt b/emails/translations/account-check_fr.txt deleted file mode 100644 index 1deeaafa15..0000000000 --- a/emails/translations/account-check_fr.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Vérification E-mail -Bonjour [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) effectue une vérification par e-mail. Accédez au lien suivant pour terminer le processus: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier. \ No newline at end of file diff --git a/emails/translations/account-check_hi.html b/emails/translations/account-check_hi.html deleted file mode 100644 index e380c72144..0000000000 --- a/emails/translations/account-check_hi.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - ईमेल सत्यापन
-
- - - - -
- [[[SERVERNAME]]] - सत्यापन -
-

हाय [[[USERNAME]]], [[[SERVERNAME]]] ईमेल सत्यापन का अनुरोध कर रहा है, प्रक्रिया को पूरा करने के लिए निम्न लिंक पर क्लिक करें।

-

- अपना ई-मेल पता सत्यापित करने के लिए यहां क्लिक करें। -

- यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें। -
\ No newline at end of file diff --git a/emails/translations/account-check_hi.txt b/emails/translations/account-check_hi.txt deleted file mode 100644 index b7a11ab342..0000000000 --- a/emails/translations/account-check_hi.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - ईमेल सत्यापन -हाय [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) ई-मेल सत्यापन कर रहा है। प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक को बताएं: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें। \ No newline at end of file diff --git a/emails/translations/account-check_hu.html b/emails/translations/account-check_hu.html deleted file mode 100644 index ee47a1ca49..0000000000 --- a/emails/translations/account-check_hu.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Email megerősítés
-
- - - - -
- [[[SERVERNAME]]] - Megerősítés -
-

Tisztelt [[[USERNAME]]], [[[SERVERNAME]]] e-mail megerősítést kér, kattintson az alábbi linkre a folyamat befejezéséhez.

-

- Kattintson ide az e-mail címének megerősítéséhez. -

- Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet. -
\ No newline at end of file diff --git a/emails/translations/account-check_hu.txt b/emails/translations/account-check_hu.txt deleted file mode 100644 index 2e42035262..0000000000 --- a/emails/translations/account-check_hu.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Email megerősítés -Tisztelt [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) e-mail ellenőrzést végez. A folyamat befejezéséhez lépjen a következő linkre: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet. \ No newline at end of file diff --git a/emails/translations/account-check_it.html b/emails/translations/account-check_it.html deleted file mode 100644 index 065a7f7e73..0000000000 --- a/emails/translations/account-check_it.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Verifica email
-
- - - - -
- [[[SERVERNAME]]] - Verifica -
-

Salve [[[USERNAME]]], [[[SERVERNAME]]] sta richiedendo la verifica tramite posta elettronica, fare clic sul collegamento seguente per completare il processo.

-

- Clicca qui per verificare il tuo indirizzo e-mail. -

- Se non hai avviato questa richiesta, ignora questo messaggio. -
\ No newline at end of file diff --git a/emails/translations/account-check_it.txt b/emails/translations/account-check_it.txt deleted file mode 100644 index b9184e525a..0000000000 --- a/emails/translations/account-check_it.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Verifica email -Salve [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) sta eseguendo una verifica e-mail. Vai al seguente link per completare il processo: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Se non hai avviato questa richiesta, ignora questo messaggio. \ No newline at end of file diff --git a/emails/translations/account-check_ja.html b/emails/translations/account-check_ja.html deleted file mode 100644 index 943e73e38a..0000000000 --- a/emails/translations/account-check_ja.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - メールの確認
-
- - - - -
- [[[SERVERNAME]]] - 確認 -
-

[[[USERNAME]]]様 [[[SERVERNAME]]] がメールの確認をリクエストしている場合は、次のリンクをクリックしてプロセスを完了してください。

-

- ここをクリックして、電子メールアドレスを確認してください。 -

- このリクエストを開始していない場合は、このメールを無視してください。 -
\ No newline at end of file diff --git a/emails/translations/account-check_ja.txt b/emails/translations/account-check_ja.txt deleted file mode 100644 index 04db0e8ca0..0000000000 --- a/emails/translations/account-check_ja.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - メールの確認 -こんにちは[[[USERNAME]]]、[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) は電子メールの検証を実行しています。プロセスを完了するには、次のリンクに移動します。 -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -このリクエストを開始していない場合は、このメールを無視してください。 \ No newline at end of file diff --git a/emails/translations/account-check_ko.html b/emails/translations/account-check_ko.html deleted file mode 100644 index fe533bb53a..0000000000 --- a/emails/translations/account-check_ko.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Email 인증
-
- - - - -
- [[[SERVERNAME]]] - 인증 -
-

안녕하세요, [[[USERNAME]]]님. [[[SERVERNAME]]] 이메일 검증을 요구하는 경우, 다음 링크를 클릭하여 과정을 완료하십시오.

-

- 이메일 주소를 검증하려면 여기를 클릭하십시오. -

- 이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오. -
\ No newline at end of file diff --git a/emails/translations/account-check_ko.txt b/emails/translations/account-check_ko.txt deleted file mode 100644 index f12607534f..0000000000 --- a/emails/translations/account-check_ko.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Email 인증 -안녕하세요, [[[USERNAME]]]님. [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]])은 이메일 검증을 위해 실행됩니다. 다음 링크로 이동하여 과정을 완료하십시오 : -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오. \ No newline at end of file diff --git a/emails/translations/account-check_nl.html b/emails/translations/account-check_nl.html deleted file mode 100644 index 6b193429c3..0000000000 --- a/emails/translations/account-check_nl.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - E-mail Verificatie
-
- - - - -
- [[[SERVERNAME]]] - Verificatie -
-

Hallo [[[USERNAME]]], [[[SERVERNAME]]] vraagt om e-mailverificatie, klik op de volgende link om het proces te voltooien.

-

- Klik hier om uw e-mailadres te verifiëren. -

- Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren. -
\ No newline at end of file diff --git a/emails/translations/account-check_nl.txt b/emails/translations/account-check_nl.txt deleted file mode 100644 index c1f236ce7e..0000000000 --- a/emails/translations/account-check_nl.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - E-mail Verificatie -Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) voert een e-mailverificatie uit. Ga naar de volgende link om het proces te voltooien: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren. \ No newline at end of file diff --git a/emails/translations/account-check_pl.html b/emails/translations/account-check_pl.html deleted file mode 100644 index 6d99d8746d..0000000000 --- a/emails/translations/account-check_pl.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Weryfikacja Email
-
- - - - -
- [[[SERVERNAME]]] - Weryfikacja -
-

Cześć [[[USERNAME]]], [[[SERVERNAME]]] prosi o weryfikację adresu email, kliknij na poniższy link, aby zakończyć proces.

-

- Kliknij tutaj, aby zweryfikować swój adres e-mail. -

- Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email. -
\ No newline at end of file diff --git a/emails/translations/account-check_pl.txt b/emails/translations/account-check_pl.txt deleted file mode 100644 index 0cadc2891e..0000000000 --- a/emails/translations/account-check_pl.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Weryfikacja Email -Cześć [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) przeprowadza weryfikację poczty elektronicznej. Kliknij na poniższy link, aby zakończyć proces: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email. \ No newline at end of file diff --git a/emails/translations/account-check_pt-br.html b/emails/translations/account-check_pt-br.html deleted file mode 100644 index 17802e2e5b..0000000000 --- a/emails/translations/account-check_pt-br.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Verificação de e-mail
-
- - - - -
- [[[SERVERNAME]]] - Verificação -
-

Olá [[[USERNAME]]], [[[SERVERNAME]]] está solicitando verificação de e-mail, clique no link a seguir para concluir o processo.

-

- Clique aqui para verificar seu endereço de e-mail. -

- Se você não iniciou esta solicitação, ignore este e-mail. -
\ No newline at end of file diff --git a/emails/translations/account-check_pt-br.txt b/emails/translations/account-check_pt-br.txt deleted file mode 100644 index 7f0be27be1..0000000000 --- a/emails/translations/account-check_pt-br.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Verificação de e-mail -Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) está realizando uma verificação de e-mail. Acesse o seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Se você não iniciou esta solicitação, ignore este e-mail. \ No newline at end of file diff --git a/emails/translations/account-check_pt.html b/emails/translations/account-check_pt.html deleted file mode 100644 index 0caea85e5c..0000000000 --- a/emails/translations/account-check_pt.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Verificação de Email
-
- - - - -
- [[[SERVERNAME]]] - Verificação -
-

Olá [[[USERNAME]]], [[[SERVERNAME]]] está solicitando verificação de e-mail, clique no link a seguir para concluir o processo.

-

- Clique aqui para verificar seu endereço de e-mail. -

- Se você não iniciou esta solicitação, ignore este e-mail. -
\ No newline at end of file diff --git a/emails/translations/account-check_pt.txt b/emails/translations/account-check_pt.txt deleted file mode 100644 index 8371a6bf4d..0000000000 --- a/emails/translations/account-check_pt.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Verificação de Email -Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) está realizando uma verificação de e-mail. Acesse o seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Se você não iniciou esta solicitação, ignore este e-mail. \ No newline at end of file diff --git a/emails/translations/account-check_ru.html b/emails/translations/account-check_ru.html deleted file mode 100644 index 875dacac76..0000000000 --- a/emails/translations/account-check_ru.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - подтверждение по электронной почте
-
- - - - -
- [[[SERVERNAME]]] - Подтверждение -
-

Привет [[[USERNAME]]], [[[SERVERNAME]]] запрашивает подтверждение по электронной почте, нажмите на следующую ссылку, чтобы завершить процесс.

-

- Нажмите здесь, чтобы подтвердить свой адрес электронной почты. -

- Если вы не инициировали этот запрос, игнорируйте это письмо. -
\ No newline at end of file diff --git a/emails/translations/account-check_ru.txt b/emails/translations/account-check_ru.txt deleted file mode 100644 index ab6dd187ea..0000000000 --- a/emails/translations/account-check_ru.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - подтверждение по электронной почте -Здравствуйте, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) выполняет проверку электронной почты. Для завершения процесса перейдите по следующей ссылке: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Если вы не инициировали этот запрос, игнорируйте это письмо. \ No newline at end of file diff --git a/emails/translations/account-check_sv.html b/emails/translations/account-check_sv.html deleted file mode 100644 index dfa7812e74..0000000000 --- a/emails/translations/account-check_sv.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - E-postverifiering
-
- - - - -
- [[[SERVERNAME]]] - Verifiering -
-

Hej [[[USERNAME]], [[[SERVERNAME]]] begär e-postverifiering, klicka på följande länk för att slutföra processen.

-

- Klicka här för att verifiera din e-postadress. -

- Om du inte initierade denna begäran, ignorerar du det här meddelandet. -
\ No newline at end of file diff --git a/emails/translations/account-check_sv.txt b/emails/translations/account-check_sv.txt deleted file mode 100644 index e5f7be09c3..0000000000 --- a/emails/translations/account-check_sv.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - E-postverifiering -Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[[URLARGS1]]]) utför en e-postverifiering. Ta bort till följande länk för att slutföra processen: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Om du inte initierade denna begäran, ignorerar du det här meddelandet. \ No newline at end of file diff --git a/emails/translations/account-check_tr.html b/emails/translations/account-check_tr.html deleted file mode 100644 index eb24fd9fb8..0000000000 --- a/emails/translations/account-check_tr.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - E-posta Doğrulaması
-
- - - - -
- [[[SERVERNAME]]] - Doğrulama -
-

Merhaba [[[USERNAME]]], [[[SERVERNAME]]] e-posta doğrulaması istiyorsa, işlemi tamamlamak için aşağıdaki bağlantıya tıklayın.

-

- E-posta adresinizi doğrulamak için burayı tıklayın. -

- Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın. -
\ No newline at end of file diff --git a/emails/translations/account-check_tr.txt b/emails/translations/account-check_tr.txt deleted file mode 100644 index b8e675d924..0000000000 --- a/emails/translations/account-check_tr.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - E-posta Doğrulaması -Merhaba [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) bir e-posta doğrulaması yapıyor. İşlemi tamamlamak için aşağıdaki bağlantıya gidin: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın. \ No newline at end of file diff --git a/emails/translations/account-check_zh-chs.html b/emails/translations/account-check_zh-chs.html deleted file mode 100644 index 49ebb48448..0000000000 --- a/emails/translations/account-check_zh-chs.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - 邮件验证
-
- - - - -
- [[[SERVERNAME]]] - 验证 -
-

[[[USERNAME]],你好, [[[SERVERNAME]]] 正在请求电邮验证,请单击以下连结以完成该过程。

-

- 单击此处以验证您的邮件地址。 -

- 如果您没有发起此请求,请忽略此邮件。 -
\ No newline at end of file diff --git a/emails/translations/account-check_zh-chs.txt b/emails/translations/account-check_zh-chs.txt deleted file mode 100644 index 38db4b5bc5..0000000000 --- a/emails/translations/account-check_zh-chs.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - 邮件验证 -嗨[[[USERNAME]]],[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在执行邮件验证。转到以下链接以完成该过程: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -如果您没有发起此请求,请忽略此邮件。 \ No newline at end of file diff --git a/emails/translations/account-check_zh-cht.html b/emails/translations/account-check_zh-cht.html deleted file mode 100644 index 4addc99d2c..0000000000 --- a/emails/translations/account-check_zh-cht.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - 電郵驗證
-
- - - - -
- [[[SERVERNAME]]] - 驗證 -
-

[[[USERNAME]],你好, [[[SERVERNAME]] 正在請求電郵驗證,請單擊以下鏈結以完成該過程。

-

- 單擊此處以驗證你的電郵地址。 -

- 如果你沒有發起此請求,請不理此電郵。 -
\ No newline at end of file diff --git a/emails/translations/account-check_zh-cht.txt b/emails/translations/account-check_zh-cht.txt deleted file mode 100644 index 9a7cb5f2b3..0000000000 --- a/emails/translations/account-check_zh-cht.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - 電郵驗證 -嗨[[[USERNAME]]],[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在執行電郵驗證。導航至以下鏈結以完成該過程: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -如果你沒有發起此請求,請不理此電郵。 \ No newline at end of file diff --git a/emails/translations/account-invite_bs.html b/emails/translations/account-invite_bs.html deleted file mode 100644 index 3ff65f3735..0000000000 --- a/emails/translations/account-invite_bs.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Pozivnica za račun
-
- - - - -
- [[[SERVERNAME]]] - Pozivnica za račun -
-

Za vas je kreiran nalog na serveru [[[SERVERNAME]]], sada mu možete pristupiti sa:

-

-    Korisničko ime: [[[ACCOUNTNAME]]]
-    Lozinka: [[[PASSWORD]]] -

- Srdačan pozdrav, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_bs.txt b/emails/translations/account-invite_bs.txt deleted file mode 100644 index d2fbb3dc64..0000000000 --- a/emails/translations/account-invite_bs.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Pozivnica za račun -Za vas je kreiran račun na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), sada mu možete pristupiti sa korisničkim imenom "[[[ACCOUNTNAME]]]" i lozinka "[[[PASSWORD]]]". -~ -Srdačan pozdrav, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_cs.html b/emails/translations/account-invite_cs.html deleted file mode 100644 index 056a73b665..0000000000 --- a/emails/translations/account-invite_cs.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Pozvánka na účet
-
- - - - -
- [[[SERVERNAME]]] - Pozvánka na účet -
-

Účet byl pro vás vytvořen na serveru [[[SERVERNAME]]], nyní k němu máte přístup:

-

-    Uživatelské jméno: [[[ACCOUNTNAME]]]
-    Heslo: [[[PASSWORD]]] -

- S pozdravem, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_cs.txt b/emails/translations/account-invite_cs.txt deleted file mode 100644 index 43c4db3267..0000000000 --- a/emails/translations/account-invite_cs.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Pozvánka na účet -Účet pro vás byl vytvořen na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), nyní k němu můžete přistupovat pomocí uživatelského jména „[[[ACCOUNTNAME]]]“ “a hesla„ [[[PASSWORD]]] ". -~ -S pozdravem, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_da.html b/emails/translations/account-invite_da.html deleted file mode 100644 index 867ae2c3db..0000000000 --- a/emails/translations/account-invite_da.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Konto invitation
-
- - - - -
- [[[SERVERNAME]]] - Konto invitation -
-

En konto blev oprettet til dig på serveren [[[SERVERNAME]]]du kan tilgå den nu med:

-

-    Brugernavn: [[[ACCOUNTNAME]]]
-    Adgangskode: [[[PASSWORD]]] -

- Venlig hilsen, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_da.txt b/emails/translations/account-invite_da.txt deleted file mode 100644 index 0c31c92a27..0000000000 --- a/emails/translations/account-invite_da.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Konto invitation -En konto blev oprettet til dig på serveren [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), du kan tilgå den nu med brugernavnet "[[[ACCOUNTNAME]]]" og adgangskode "[[[PASSWORD]]]". -~ -Venlig hilsen, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_de.html b/emails/translations/account-invite_de.html deleted file mode 100644 index 96bfb7e7f8..0000000000 --- a/emails/translations/account-invite_de.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Kontoeinladung
-
- - - - -
- [[[SERVERNAME]]] - Kontoeinladung -
-

Auf dem Server wurde ein Konto für Sie erstellt [[[SERVERNAME]]], Sie können jetzt darauf zugreifen mit:

-

-    Benutzername: [[[ACCOUNTNAME]]]
-    Passwort: [[[PASSWORD]]] -

- Freundliche Grüße, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_de.txt b/emails/translations/account-invite_de.txt deleted file mode 100644 index eaed8c0667..0000000000 --- a/emails/translations/account-invite_de.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Kontoeinladung -Auf dem Server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) wurde ein Konto für Sie erstellt, Sie können ab sofort mit dem Benutzernamen "[[[ACCOUNTNAME]]]" und dem Passwort "[[[PASSWORD]]]" darauf zugreifen. -~ -Freundliche Grüße, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_es.html b/emails/translations/account-invite_es.html deleted file mode 100644 index b10e0a5668..0000000000 --- a/emails/translations/account-invite_es.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Invitación de Cuenta
-
- - - - -
- [[[SERVERNAME]]] - Invitación de Cuenta -
-

Una cuenta ha sido creada en tu servidor [[[SERVERNAME]]], ahora puede acceder con:

-

-    Nombre de Usuario: [[[ACCOUNTNAME]]]
-    Contraseña: [[[PASSWORD]]] -

- Atentamente, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_es.txt b/emails/translations/account-invite_es.txt deleted file mode 100644 index 69d6490037..0000000000 --- a/emails/translations/account-invite_es.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Invitación de Cuenta -Una cuenta ha sido creada en tu servidor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), ahora puedes acceder con el usuario "[[[ACCOUNTNAME]]]" y la contraseña "[[[PASSWORD]]]". -~ -Atentamente, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_fi.html b/emails/translations/account-invite_fi.html deleted file mode 100644 index b779860625..0000000000 --- a/emails/translations/account-invite_fi.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Tili kutsu
-
- - - - -
- [[[SERVERNAME]]] - Tili kutsu -
-

Sinulle on luotu tili palvelimelle [[[SERVERNAME]]], voit käyttää sitä nyt:

-

-    Käyttäjätunnus: [[[ACCOUNTNAME]]]
-    Salasana: [[[PASSWORD]]] -

- Ystävällisin terveisin, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_fi.txt b/emails/translations/account-invite_fi.txt deleted file mode 100644 index bb9531207d..0000000000 --- a/emails/translations/account-invite_fi.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Tili kutsu -Sinulle on luotu tili palvelimelle [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), voit käyttää sitä nyt käyttäjätunnuksella "[[[ACCOUNTNAME]]]" and salasanalla "[[[PASSWORD]]]". -~ -Ystävällisin terveisin, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_fr.html b/emails/translations/account-invite_fr.html deleted file mode 100644 index 2eb85ab02d..0000000000 --- a/emails/translations/account-invite_fr.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Invitation au compte
-
- - - - -
- [[[SERVERNAME]]] - Invitation au compte -
-

Un compte a été créé pour vous sur le serveur [[[SERVERNAME]]], vous pouvez y accéder maintenant avec :

-

-    Nom d'utilisateur: [[[ACCOUNTNAME]]]
-    Mot de passe: [[[PASSWORD]]] -

- Meilleures salutations, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_fr.txt b/emails/translations/account-invite_fr.txt deleted file mode 100644 index 2b2b6a0932..0000000000 --- a/emails/translations/account-invite_fr.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Invitation au compte -Un compte a été créé pour vous sur le serveur [[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), vous pouvez y accéder maintenant avec le nom d'utilisateur "[[[ACCOUNTNAME]]]" et le mot de passe "[[[PASSWORD]]]". -~ -Meilleures salutations, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_hi.html b/emails/translations/account-invite_hi.html deleted file mode 100644 index b1d529f718..0000000000 --- a/emails/translations/account-invite_hi.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - खाता निमंत्रण
-
- - - - -
- [[[SERVERNAME]]] - खाता निमंत्रण -
-

सर्वर पर आपके लिए एक खाता बनाया गया था [[[SERVERNAME]]], आप इसे अभी एक्सेस कर सकते हैं:

-

-    उपयोगकर्ता नाम: [[[ACCOUNTNAME]]]
-    कुंजिका: [[[PASSWORD]]] -

- सादर, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_hi.txt b/emails/translations/account-invite_hi.txt deleted file mode 100644 index f1e09f0f0d..0000000000 --- a/emails/translations/account-invite_hi.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - खाता निमंत्रण -सर्वर [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) पर आपके लिए एक खाता बनाया गया था, आप इसे अब उपयोगकर्ता नाम "[[[ACCOUNTNAME]]]]" और पासवर्ड "[[[PASSWORD]]]" के साथ एक्सेस कर सकते हैं।। -~ -सादर, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_hu.html b/emails/translations/account-invite_hu.html deleted file mode 100644 index b615171009..0000000000 --- a/emails/translations/account-invite_hu.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Fiók meghívó
-
- - - - -
- [[[SERVERNAME]]] - Fiók meghívó -
-

Létrehozásra került Önnek egy fiók a kiszolgálón [[[SERVERNAME]]], most a következőkkel érheti el:

-

-    Felhasználó név: [[[ACCOUNTNAME]]]
-    Jelszó: [[[PASSWORD]]] -

- Üdvözlettel, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_hu.txt b/emails/translations/account-invite_hu.txt deleted file mode 100644 index 6b85d933ec..0000000000 --- a/emails/translations/account-invite_hu.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Fiók meghívó -A [[[SERVERNAME]]] szerveren ([[[SERVERURL]]]/[[[URLARGS1]]]) létrehozásra került egy fiókot az Ön számára, amelyhez most már hozzáférhet a "[[[[ACCOUNTNAME]]]" felhasználónévvel és a "[[[[PASSWORD]]]" jelszóval. -~ -Üdvözlettel, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_it.html b/emails/translations/account-invite_it.html deleted file mode 100644 index 08fcdd876d..0000000000 --- a/emails/translations/account-invite_it.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Invito per l'account
-
- - - - -
- [[[SERVERNAME]]] - Invito per l'account -
-

È stato creato un account per te sul server [[[SERVERNAME]]], puoi accedervi ora con

-

-    Nome utente: [[[ACCOUNTNAME]]]
-    Password: [[[PASSWORD]]] -

- I migliori saluti, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_it.txt b/emails/translations/account-invite_it.txt deleted file mode 100644 index 0e9242cbf6..0000000000 --- a/emails/translations/account-invite_it.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Invito per l'account -È stato creato un account per te sul server [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]]), puoi accedervi ora con il nome utente "[[[ACCOUNTNAME]]]" e password "[[[PASSWORD]]]". -~ -I migliori saluti, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_ja.html b/emails/translations/account-invite_ja.html deleted file mode 100644 index 65af4f1941..0000000000 --- a/emails/translations/account-invite_ja.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - アカウントの招待
-
- - - - -
- [[[SERVERNAME]]] - アカウントの招待 -
-

サーバー上にアカウントが作成されました [[[SERVERNAME]]]、あなたは今それを使ってそれにアクセスできます:

-

-    ユーザー名: [[[ACCOUNTNAME]]]
-    パスワード: [[[PASSWORD]]] -

- 宜しくお願いします、 -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_ja.txt b/emails/translations/account-invite_ja.txt deleted file mode 100644 index b689fdb233..0000000000 --- a/emails/translations/account-invite_ja.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - アカウントの招待 -サーバー[[[SERVERNAME]]]([[[SERVERURL]]]/[[[URLARGS1]]])にアカウントが作成されました。ユーザー名 "[[[ACCOUNTNAME]]]"とパスワード "[[[PASSWORD] ]] "。 -~ -宜しくお願いします、 -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_ko.html b/emails/translations/account-invite_ko.html deleted file mode 100644 index e04faafa0e..0000000000 --- a/emails/translations/account-invite_ko.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - 계정 초대
-
- - - - -
- [[[SERVERNAME]]] - 계정 초대 -
-

당신을 위해 서버에서 한 계정이 생성되었습니다. [[[SERVERNAME]]]이제 당신은 다음으로 접근 가능합니다:

-

-    사용자 이름: [[[ACCOUNTNAME]]]
-    암호: [[[PASSWORD]]] -

- 최고의 안부를 전합니다, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_ko.txt b/emails/translations/account-invite_ko.txt deleted file mode 100644 index 348270a5b9..0000000000 --- a/emails/translations/account-invite_ko.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - 계정 초대 -당신을 위해 서버에서 한 계정이 생성되었습니다 : [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), 이제 당신은 다음 이름으로 접근 가능합니다 : "[[[ACCOUNTNAME]]]" 그리고 비밀번호는 다음과 같습니다 : "[[[PASSWORD]]]". -~ -최고의 안부를 전합니다, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_nl.html b/emails/translations/account-invite_nl.html deleted file mode 100644 index 814f1532ce..0000000000 --- a/emails/translations/account-invite_nl.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Account uitnodiging
-
- - - - -
- [[[SERVERNAME]]] - Account uitnodiging -
-

Er is een account voor je aangemaakt op de server [[[SERVERNAME]]], je hebt er nu toegang toe met:

-

-    Gebruikersnaam: [[[ACCOUNTNAME]]]
-    Wachtwoord: [[[PASSWORD]]] -

- Vriendelijke groeten, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_nl.txt b/emails/translations/account-invite_nl.txt deleted file mode 100644 index 6de3dff1a1..0000000000 --- a/emails/translations/account-invite_nl.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Account uitnodiging -Er is een account jouw aangemaakt op de server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), Je kan inloggen met de gebruikersnaam "[[[ACCOUNTNAME]]]" en wachtwoord "[[[PASSWORD]]]". -~ -Vriendelijke groeten, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_pl.html b/emails/translations/account-invite_pl.html deleted file mode 100644 index daa59a3144..0000000000 --- a/emails/translations/account-invite_pl.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Zaproszenie Do Założenia Konta
-
- - - - -
- [[[SERVERNAME]]] - Zaproszenie Do Założenia Konta -
-

Konto zostało utworzone dla Ciebie na serwerze [[[SERVERNAME]]], możesz uzyskać dostęp przez:

-

-    Nazwa użytkownika: [[[ACCOUNTNAME]]]
-    Hasło: [[[PASSWORD]]] -

- Z wyrazami szacunku, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_pl.txt b/emails/translations/account-invite_pl.txt deleted file mode 100644 index aab5ab4ed0..0000000000 --- a/emails/translations/account-invite_pl.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Zaproszenie Do Założenia Konta -Konto zostało utworzone dla Ciebie na serwerze [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), możesz uzyskać do niego dostęp podając nazwę użytkownika "[[[ACCOUNTNAME]]]" i hasło "[[[PASSWORD]]]". -~ -Z wyrazami szacunku, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_pt-br.html b/emails/translations/account-invite_pt-br.html deleted file mode 100644 index 302ada86b9..0000000000 --- a/emails/translations/account-invite_pt-br.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Convite para conta
-
- - - - -
- [[[SERVERNAME]]] - Convite para conta -
-

Uma conta foi criada para você no servidor [[[SERVERNAME]]], você pode acessá-lo agora com:

-

-    Nome do usuário: [[[ACCOUNTNAME]]]
-    Senha: [[[PASSWORD]]] -

- Atenciosamente, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_pt-br.txt b/emails/translations/account-invite_pt-br.txt deleted file mode 100644 index be1d1e3c1d..0000000000 --- a/emails/translations/account-invite_pt-br.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Convite para conta -Uma conta foi criada para você no servidor [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]]), você pode acessá-la agora com o nome de usuário "[[[ACCOUNTNAME]]]" e senha "[[[PASSWORD]]]". -~ -Atenciosamente, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_pt.html b/emails/translations/account-invite_pt.html deleted file mode 100644 index d804ac67a7..0000000000 --- a/emails/translations/account-invite_pt.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Convite para conta
-
- - - - -
- [[[SERVERNAME]]] - Convite para conta -
-

Uma conta foi criada para você no servidor [[[SERVERNAME]]], pode aceder agora com:

-

-    Nome de usuário: [[[ACCOUNTNAME]]]
-    Senha: [[[PASSWORD]]] -

- Cumprimentos, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_pt.txt b/emails/translations/account-invite_pt.txt deleted file mode 100644 index b9d0e618a0..0000000000 --- a/emails/translations/account-invite_pt.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Convite para conta -Uma conta foi criada para você no servidor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), você pode acessá-la agora com o nome de usuário "[[[ACCOUNTNAME]]]" e a senha "[[[PASSWORD] ]] ". -~ -Cumprimentos, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_ru.html b/emails/translations/account-invite_ru.html deleted file mode 100644 index 308e6a82ea..0000000000 --- a/emails/translations/account-invite_ru.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - приглашение в аккаунт
-
- - - - -
- [[[SERVERNAME]]] - приглашение в аккаунт -
-

Для вас была создана учетная запись на сервере [[[SERVERNAME]]]Вы можете получить к нему доступ сейчас:

-

-    Имя пользователя: [[[ACCOUNTNAME]]]
-    Пароль: [[[PASSWORD]]] -

- С уважением, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_ru.txt b/emails/translations/account-invite_ru.txt deleted file mode 100644 index b9814a0289..0000000000 --- a/emails/translations/account-invite_ru.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - приглашение в аккаунт -Для вас на сервере [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) была создана учетная запись, теперь вы можете получить к ней доступ с именем пользователя «[[[ACCOUNTNAME]]]» и паролем «[[[PASSWORD]]]". -~ -С уважением, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_sv.html b/emails/translations/account-invite_sv.html deleted file mode 100644 index 3dd6c3e34a..0000000000 --- a/emails/translations/account-invite_sv.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Kontoinbjudan
-
- - - - -
- [[[SERVERNAME]]] - Kontoinbjudan -
-

Ett konto skapades åt dig på servern [[[SERVERNAME]]]kan du komma åt det nu med:

-

-    Användarnamn: [[[ACCOUNTNAME]]]
-    Lösenord: [[[PASSWORD]]] -

- Vänliga hälsningar, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_sv.txt b/emails/translations/account-invite_sv.txt deleted file mode 100644 index e95330d9b1..0000000000 --- a/emails/translations/account-invite_sv.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Kontoinbjudan -Ett konto skapades åt dig på servern [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]], du kan komma åt det nu med användarnamnet "[[[ACCOUNTNAME]]]" och lösenord "[[[PASSWORD]]]". -~ -Vänliga hälsningar, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_tr.html b/emails/translations/account-invite_tr.html deleted file mode 100644 index 7ba1915f07..0000000000 --- a/emails/translations/account-invite_tr.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - Hesap Davetiyesi
-
- - - - -
- [[[SERVERNAME]]] - Hesap Davetiyesi -
-

Sunucuda sizin için bir hesap oluşturuldu [[[SERVERNAME]]], şimdi şununla erişebilirsiniz:

-

-    Kullanıcı adı: [[[ACCOUNTNAME]]]
-    Parola: [[[PASSWORD]]] -

- Saygılarımla, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_tr.txt b/emails/translations/account-invite_tr.txt deleted file mode 100644 index 49e30567f9..0000000000 --- a/emails/translations/account-invite_tr.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - Hesap Davetiyesi -[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) sunucusunda sizin için bir hesap oluşturuldu, şimdi "[[[ACCOUNTNAME]]]" kullanıcı adı ve "[[[PASSWORD]]]" ile bu hesaba erişebilirsiniz. -~ -Saygılarımla, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_zh-chs.html b/emails/translations/account-invite_zh-chs.html deleted file mode 100644 index 256c34e484..0000000000 --- a/emails/translations/account-invite_zh-chs.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - 帐户邀请
-
- - - - -
- [[[SERVERNAME]]] - 帐户邀请 -
-

在服务器上为您创建了一个帐户 [[[SERVERNAME]]],您现在可以通过以下方式访问它:

-

-    用户名: [[[ACCOUNTNAME]]]
-    密码: [[[PASSWORD]]] -

- 最好的祝福, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_zh-chs.txt b/emails/translations/account-invite_zh-chs.txt deleted file mode 100644 index 89ea6e62e1..0000000000 --- a/emails/translations/account-invite_zh-chs.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - 帐户邀请 -已在服务器[[[SERVERNAME]]]([[[SERVERURL]]]/[[[URLARGS1]]])上为您创建了一个帐户,您现在可以使用用户名“ [[[ACCOUNTNAME]]]”和密码“ [[[PASSWORD]]]”。 -~ -最好的祝福, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-invite_zh-cht.html b/emails/translations/account-invite_zh-cht.html deleted file mode 100644 index 3b7f71c670..0000000000 --- a/emails/translations/account-invite_zh-cht.html +++ /dev/null @@ -1,19 +0,0 @@ -
[[[SERVERNAME]]] - 帳戶邀請
-
- - - - -
- [[[SERVERNAME]]] - 帳戶邀請 -
-

在伺服器上為你創建了一個帳戶 [[[SERVERNAME]]],你現在可以通過以下方式訪問它:

-

-    用戶名: [[[ACCOUNTNAME]]]
-    密碼: [[[PASSWORD]]] -

- 最好的祝福, -
- [[[USERNAME]]] -
-
\ No newline at end of file diff --git a/emails/translations/account-invite_zh-cht.txt b/emails/translations/account-invite_zh-cht.txt deleted file mode 100644 index 5af1127485..0000000000 --- a/emails/translations/account-invite_zh-cht.txt +++ /dev/null @@ -1,5 +0,0 @@ -[[[SERVERNAME]]] - 帳戶邀請 -在伺服器[[[SERVERNAME]]]([[[SERVERURL]]]/[[[URLARGS1]]])上為你創建了一個帳戶,你現在可以使用用戶名“ [[[ACCOUNTNAME]]]”和密碼“ [[[PASSWORD]]]”。 -~ -最好的祝福, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/account-login_bs.html b/emails/translations/account-login_bs.html deleted file mode 100644 index 0a65c8e75f..0000000000 --- a/emails/translations/account-login_bs.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Prijava na račun
-
- - - - -
- [[[SERVERNAME]]] - Prijava na račun -
-

Vaš token za prijavu je: [[[TOKEN]]]

-

Ovaj token se može koristiti samo jednom i vrijedi 5 minuta.

-
\ No newline at end of file diff --git a/emails/translations/account-login_bs.txt b/emails/translations/account-login_bs.txt deleted file mode 100644 index 5d3ae8a86b..0000000000 --- a/emails/translations/account-login_bs.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Prijava na račun -Vaš token za prijavu je: [[[TOKEN]]] -~ -Ovaj token se može koristiti samo jednom i vrijedi 5 minuta. \ No newline at end of file diff --git a/emails/translations/account-login_cs.html b/emails/translations/account-login_cs.html deleted file mode 100644 index 00c8f5e4d1..0000000000 --- a/emails/translations/account-login_cs.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Přihlášení k účtu
-
- - - - -
- [[[SERVERNAME]]] - Přihlášení k účtu -
-

Váš přihlašovací token je: [[[TOKEN]]]

-

Tento token lze použít pouze jednou a je platný po dobu 5 minut.

-
\ No newline at end of file diff --git a/emails/translations/account-login_cs.txt b/emails/translations/account-login_cs.txt deleted file mode 100644 index 266ad4504c..0000000000 --- a/emails/translations/account-login_cs.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Přihlášení k účtu -Váš přihlašovací token je: [[[TOKEN]]] -~ -Tento token lze použít pouze jednou a je platný po dobu 5 minut. \ No newline at end of file diff --git a/emails/translations/account-login_da.html b/emails/translations/account-login_da.html deleted file mode 100644 index ea3efa099d..0000000000 --- a/emails/translations/account-login_da.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Konto login
-
- - - - -
- [[[SERVERNAME]]] - Konto login -
-

Dit login-token er: [[[TOKEN]]]

-

Denne token kan kun bruges én gang og er gyldig i 5 minutter.

-
\ No newline at end of file diff --git a/emails/translations/account-login_da.txt b/emails/translations/account-login_da.txt deleted file mode 100644 index f6a8b44f19..0000000000 --- a/emails/translations/account-login_da.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Konto login -Dit login-token er: [[[TOKEN]]] -~ -Denne token kan kun bruges én gang og er gyldig i 5 minutter. \ No newline at end of file diff --git a/emails/translations/account-login_de.html b/emails/translations/account-login_de.html deleted file mode 100644 index 9047666c00..0000000000 --- a/emails/translations/account-login_de.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Konto-Login
-
- - - - -
- [[[SERVERNAME]]] - Konto-Login -
-

Ihr Anmelde-Token lautet: [[[TOKEN]]]

-

Dieser Token kann nur einmal verwendet werden und ist 5 Minuten gültig.

-
\ No newline at end of file diff --git a/emails/translations/account-login_de.txt b/emails/translations/account-login_de.txt deleted file mode 100644 index 0f97eb947b..0000000000 --- a/emails/translations/account-login_de.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Konto-Login -Ihr Anmelde-Token lautet: [[[TOKEN]]] -~ -Dieser Token kann nur einmal verwendet werden und ist 5 Minuten gültig. \ No newline at end of file diff --git a/emails/translations/account-login_es.html b/emails/translations/account-login_es.html deleted file mode 100644 index 20f603a13a..0000000000 --- a/emails/translations/account-login_es.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Inicio de Sesión
-
- - - - -
- [[[SERVERNAME]]] - Inicio de Sesión -
-

Tu token de inicio de sesión es: [[[TOKEN]]]

-

Este token sólo se puede usar una vez y es válido durante 5 minutos.

-
\ No newline at end of file diff --git a/emails/translations/account-login_es.txt b/emails/translations/account-login_es.txt deleted file mode 100644 index 290ecb94f2..0000000000 --- a/emails/translations/account-login_es.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Inicio de Sesión -Tu token de inicio de sesión es: [[[TOKEN]]] -~ -Este token sólo se puede usar una vez y es válido durante 5 minutos. \ No newline at end of file diff --git a/emails/translations/account-login_fi.html b/emails/translations/account-login_fi.html deleted file mode 100644 index 3dc496ea89..0000000000 --- a/emails/translations/account-login_fi.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Tilille Kirjautuminen
-
- - - - -
- [[[SERVERNAME]]] - Tilille Kirjautuminen -
-

Kirjautumistunnuksesi on: [[[TOKEN]]]

-

Tätä tunnusta voidaan käyttää vain kerran ja se on voimassa 5 minuuttia.

-
\ No newline at end of file diff --git a/emails/translations/account-login_fi.txt b/emails/translations/account-login_fi.txt deleted file mode 100644 index 87ec2ff3ad..0000000000 --- a/emails/translations/account-login_fi.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Tilille Kirjautuminen -Kirjautumistunnuksesi on: [[[TOKEN]]] -~ -Tätä tunnusta voidaan käyttää vain kerran ja se on voimassa 5 minuuttia. \ No newline at end of file diff --git a/emails/translations/account-login_fr.html b/emails/translations/account-login_fr.html deleted file mode 100644 index 16c14f7df3..0000000000 --- a/emails/translations/account-login_fr.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Connexion au compte
-
- - - - -
- [[[SERVERNAME]]] - Connexion au compte -
-

Votre jeton de connexion est: [[[TOKEN]]]

-

Ce jeton ne peut être utilisé qu'une seule fois et est valide pendant 5 minutes.

-
\ No newline at end of file diff --git a/emails/translations/account-login_fr.txt b/emails/translations/account-login_fr.txt deleted file mode 100644 index c1813f23dc..0000000000 --- a/emails/translations/account-login_fr.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Connexion au compte -Votre jeton de connexion est: [[[TOKEN]]] -~ -Ce jeton ne peut être utilisé qu'une seule fois et est valide pendant 5 minutes. \ No newline at end of file diff --git a/emails/translations/account-login_hi.html b/emails/translations/account-login_hi.html deleted file mode 100644 index 2144013166..0000000000 --- a/emails/translations/account-login_hi.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - खाता लॉगिन
-
- - - - -
- [[[SERVERNAME]]] - खाता लॉगिन -
-

आपका लॉगिन टोकन है: [[[TOKEN]]]

-

यह टोकन केवल एक बार उपयोग किया जा सकता है और 5 मिनट के लिए वैध है।

-
\ No newline at end of file diff --git a/emails/translations/account-login_hi.txt b/emails/translations/account-login_hi.txt deleted file mode 100644 index cac0e1899d..0000000000 --- a/emails/translations/account-login_hi.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - खाता लॉगिन -आपका लॉगिन टोकन है: [[[TOKEN]]] -~ -यह टोकन केवल एक बार उपयोग किया जा सकता है और 5 मिनट के लिए वैध है। \ No newline at end of file diff --git a/emails/translations/account-login_hu.html b/emails/translations/account-login_hu.html deleted file mode 100644 index e8c93ae323..0000000000 --- a/emails/translations/account-login_hu.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Bejelentkezés a fiókba
-
- - - - -
- [[[SERVERNAME]]] - Bejelentkezés a fiókba -
-

Az Ön bejelentkezési tokenje: [[[TOKEN]]]

-

Ez a token csak egyszer használható, és 5 percig érvényes.

-
\ No newline at end of file diff --git a/emails/translations/account-login_hu.txt b/emails/translations/account-login_hu.txt deleted file mode 100644 index 0b104a0900..0000000000 --- a/emails/translations/account-login_hu.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Bejelentkezés a fiókba -Az Ön bejelentkezési tokenje: [[[TOKEN]]] -~ -Ez a token csak egyszer használható, és 5 percig érvényes. \ No newline at end of file diff --git a/emails/translations/account-login_it.html b/emails/translations/account-login_it.html deleted file mode 100644 index ccc5302210..0000000000 --- a/emails/translations/account-login_it.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Accesso all'account
-
- - - - -
- [[[SERVERNAME]]] - Accesso all'account -
-

Il tuo token di accesso è: [[[TOKEN]]]

-

Questo token può essere utilizzato solo una volta ed è valido per 5 minuti.

-
\ No newline at end of file diff --git a/emails/translations/account-login_it.txt b/emails/translations/account-login_it.txt deleted file mode 100644 index 22b4bf105e..0000000000 --- a/emails/translations/account-login_it.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Accesso all'account -Il tuo token di accesso è: [[[TOKEN]]] -~ -Questo token può essere utilizzato solo una volta ed è valido per 5 minuti. \ No newline at end of file diff --git a/emails/translations/account-login_ja.html b/emails/translations/account-login_ja.html deleted file mode 100644 index 2670cffe2f..0000000000 --- a/emails/translations/account-login_ja.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - アカウントログイン
-
- - - - -
- [[[SERVERNAME]]] - アカウントログイン -
-

ログイントークンは次のとおりです:[[[TOKEN]]]

-

このトークンは1回だけ使用でき、5分間有効です。

-
\ No newline at end of file diff --git a/emails/translations/account-login_ja.txt b/emails/translations/account-login_ja.txt deleted file mode 100644 index aeda87ef4f..0000000000 --- a/emails/translations/account-login_ja.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - アカウントログイン -ログイントークンは次のとおりです:[[[TOKEN]]] -~ -このトークンは1回だけ使用でき、5分間有効です。 \ No newline at end of file diff --git a/emails/translations/account-login_ko.html b/emails/translations/account-login_ko.html deleted file mode 100644 index cec867ca8a..0000000000 --- a/emails/translations/account-login_ko.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - 계정 로그인
-
- - - - -
- [[[SERVERNAME]]] - 계정 로그인 -
-

당신이 로그인한 토큰은 다음과 같습니다 : [[[TOKEN]]]

-

이 토큰은 오직 한 번만 사용될 수 있으며, 5분 동안만 유효합니다.

-
\ No newline at end of file diff --git a/emails/translations/account-login_ko.txt b/emails/translations/account-login_ko.txt deleted file mode 100644 index 6540818142..0000000000 --- a/emails/translations/account-login_ko.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - 계정 로그인 -당신이 로그인한 토큰은 다음과 같습니다 : [[[TOKEN]]] -~ -이 토큰은 오직 한 번만 사용될 수 있으며, 5분 동안만 유효합니다. \ No newline at end of file diff --git a/emails/translations/account-login_nl.html b/emails/translations/account-login_nl.html deleted file mode 100644 index bc5d7e2a8d..0000000000 --- a/emails/translations/account-login_nl.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Inloggen account
-
- - - - -
- [[[SERVERNAME]]] - Inloggen account -
-

Uw login token is: [[[TOKEN]]]

-

Dit token kan maar één keer worden gebruikt en is 5 minuten geldig.

-
\ No newline at end of file diff --git a/emails/translations/account-login_nl.txt b/emails/translations/account-login_nl.txt deleted file mode 100644 index 2aeadd65a8..0000000000 --- a/emails/translations/account-login_nl.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Inloggen account -Uw login token is: [[[TOKEN]]] -~ -Dit token kan maar één keer worden gebruikt en is 5 minuten geldig. \ No newline at end of file diff --git a/emails/translations/account-login_pl.html b/emails/translations/account-login_pl.html deleted file mode 100644 index dc43fc0901..0000000000 --- a/emails/translations/account-login_pl.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Logowanie Do Konta
-
- - - - -
- [[[SERVERNAME]]] - Logowanie Do Konta -
-

Twój token logowania to: [[[TOKEN]]]

-

Ten token może być użyty tylko raz i jest ważny przez 5 minut.

-
\ No newline at end of file diff --git a/emails/translations/account-login_pl.txt b/emails/translations/account-login_pl.txt deleted file mode 100644 index 28cb3225e8..0000000000 --- a/emails/translations/account-login_pl.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Logowanie Do Konta -Twój token logowania to: [[[TOKEN]]] -~ -Ten token może być użyty tylko raz i jest ważny przez 5 minut. \ No newline at end of file diff --git a/emails/translations/account-login_pt-br.html b/emails/translations/account-login_pt-br.html deleted file mode 100644 index 7bb5973d13..0000000000 --- a/emails/translations/account-login_pt-br.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Login da conta
-
- - - - -
- [[[SERVERNAME]]] - Login da conta -
-

Seu token de login é: [[[TOKEN]]]

-

Este token só pode ser usado uma vez e é válido por 5 minutos.

-
\ No newline at end of file diff --git a/emails/translations/account-login_pt-br.txt b/emails/translations/account-login_pt-br.txt deleted file mode 100644 index ebfbc919e0..0000000000 --- a/emails/translations/account-login_pt-br.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Login da conta -Seu token de login é: [[[TOKEN]]] -~ -Este token só pode ser usado uma vez e é válido por 5 minutos. \ No newline at end of file diff --git a/emails/translations/account-login_pt.html b/emails/translations/account-login_pt.html deleted file mode 100644 index 7bb5973d13..0000000000 --- a/emails/translations/account-login_pt.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Login da conta
-
- - - - -
- [[[SERVERNAME]]] - Login da conta -
-

Seu token de login é: [[[TOKEN]]]

-

Este token só pode ser usado uma vez e é válido por 5 minutos.

-
\ No newline at end of file diff --git a/emails/translations/account-login_pt.txt b/emails/translations/account-login_pt.txt deleted file mode 100644 index ebfbc919e0..0000000000 --- a/emails/translations/account-login_pt.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Login da conta -Seu token de login é: [[[TOKEN]]] -~ -Este token só pode ser usado uma vez e é válido por 5 minutos. \ No newline at end of file diff --git a/emails/translations/account-login_ru.html b/emails/translations/account-login_ru.html deleted file mode 100644 index c1b2dd46b0..0000000000 --- a/emails/translations/account-login_ru.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Вход в аккаунт
-
- - - - -
- [[[SERVERNAME]]] - Вход в аккаунт -
-

Ваш токен для входа: [[[TOKEN]]]

-

Этот токен может быть использован только один раз и действителен в течение 5 минут.

-
\ No newline at end of file diff --git a/emails/translations/account-login_ru.txt b/emails/translations/account-login_ru.txt deleted file mode 100644 index 54fca89500..0000000000 --- a/emails/translations/account-login_ru.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Вход в аккаунт -Ваш токен для входа: [[[TOKEN]]] -~ -Этот токен может быть использован только один раз и действителен в течение 5 минут. \ No newline at end of file diff --git a/emails/translations/account-login_sv.html b/emails/translations/account-login_sv.html deleted file mode 100644 index 017416db05..0000000000 --- a/emails/translations/account-login_sv.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Kontoinloggning
-
- - - - -
- [[[SERVERNAME]]] - Kontoinloggning -
-

Din inloggningstoken är: [[[TOKEN]]]

-

Denna token kan bara användas en gång och är giltig i 5 minuter.

-
\ No newline at end of file diff --git a/emails/translations/account-login_sv.txt b/emails/translations/account-login_sv.txt deleted file mode 100644 index 4ca1ee6986..0000000000 --- a/emails/translations/account-login_sv.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Kontoinloggning -Din inloggningstoken är: [[[TOKEN]]] -~ -Denna token kan bara användas en gång och är giltig i 5 minuter. \ No newline at end of file diff --git a/emails/translations/account-login_tr.html b/emails/translations/account-login_tr.html deleted file mode 100644 index e3f19929e3..0000000000 --- a/emails/translations/account-login_tr.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - Hesap Girişi
-
- - - - -
- [[[SERVERNAME]]] - Hesap Girişi -
-

Giriş anahtarınız: [[[TOKEN]]]

-

Bu simge yalnızca bir kez kullanılabilir ve 5 dakika geçerlidir.

-
\ No newline at end of file diff --git a/emails/translations/account-login_tr.txt b/emails/translations/account-login_tr.txt deleted file mode 100644 index b37d8b57a9..0000000000 --- a/emails/translations/account-login_tr.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - Hesap Girişi -Giriş anahtarınız: [[[TOKEN]]] -~ -Bu simge yalnızca bir kez kullanılabilir ve 5 dakika geçerlidir. \ No newline at end of file diff --git a/emails/translations/account-login_zh-chs.html b/emails/translations/account-login_zh-chs.html deleted file mode 100644 index 82d896cef4..0000000000 --- a/emails/translations/account-login_zh-chs.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - 帐户登录
-
- - - - -
- [[[SERVERNAME]]] - 帐户登录 -
-

您的登录保安编码为:[[[TOKEN]]]

-

此保安编码只能使用一次,有效期为5分钟。

-
\ No newline at end of file diff --git a/emails/translations/account-login_zh-chs.txt b/emails/translations/account-login_zh-chs.txt deleted file mode 100644 index 2dc58cb421..0000000000 --- a/emails/translations/account-login_zh-chs.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - 帐户登录 -您的登录保安编码为:[[[TOKEN]]] -~ -此保安编码只能使用一次,有效期为5分钟。 \ No newline at end of file diff --git a/emails/translations/account-login_zh-cht.html b/emails/translations/account-login_zh-cht.html deleted file mode 100644 index 876e41d4b6..0000000000 --- a/emails/translations/account-login_zh-cht.html +++ /dev/null @@ -1,12 +0,0 @@ -
[[[SERVERNAME]]] - 帳戶登入
-
- - - - -
- [[[SERVERNAME]]] - 帳戶登入 -
-

你的登入保安編碼為:[[[TOKEN]]]

-

該保安編碼只能使用一次,有效期為5分鐘。

-
\ No newline at end of file diff --git a/emails/translations/account-login_zh-cht.txt b/emails/translations/account-login_zh-cht.txt deleted file mode 100644 index adeb97ddf0..0000000000 --- a/emails/translations/account-login_zh-cht.txt +++ /dev/null @@ -1,4 +0,0 @@ -[[[SERVERNAME]]] - 帳戶登入 -你的登入保安編碼為:[[[TOKEN]]] -~ -該保安編碼只能使用一次,有效期為5分鐘。 \ No newline at end of file diff --git a/emails/translations/account-reset_bs.html b/emails/translations/account-reset_bs.html deleted file mode 100644 index 36138d24f2..0000000000 --- a/emails/translations/account-reset_bs.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Resetiranje računa
-
- - - - -
- [[[SERVERNAME]]] - Potvrda -
-

Zdravo [[[USERNAME]]], [[[SERVERNAME]]] traži resetiranje lozinke računa, kliknite na sljedeću vezu da dovršite proces.

-

- Kliknite ovdje da poništite lozinku za svoj račun. -

- Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku. -
\ No newline at end of file diff --git a/emails/translations/account-reset_bs.txt b/emails/translations/account-reset_bs.txt deleted file mode 100644 index fe99cfa6ad..0000000000 --- a/emails/translations/account-reset_bs.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Resetiranje računa -Zdravo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) traži reset lozinke naloga. Idite na sljedeću vezu da dovršite proces: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku. \ No newline at end of file diff --git a/emails/translations/account-reset_cs.html b/emails/translations/account-reset_cs.html deleted file mode 100644 index f5b5cae18a..0000000000 --- a/emails/translations/account-reset_cs.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Reset účtu
-
- - - - -
- [[[SERVERNAME]]] - Ověření -
-

Ahoj [[[USERNAME]]], [[[SERVERNAME]]] požaduje obnovení hesla k účtu, dokončete proces kliknutím na následující odkaz.

-

- Kliknutím sem obnovíte heslo svého účtu. -

- Pokud jste tento požadavek nezačali, ignorujte tento e-mail. -
\ No newline at end of file diff --git a/emails/translations/account-reset_cs.txt b/emails/translations/account-reset_cs.txt deleted file mode 100644 index bdd89774e4..0000000000 --- a/emails/translations/account-reset_cs.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Reset účtu -Ahoj [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) požaduje obnovení hesla k účtu. Chcete-li proces dokončit, přejděte na následující odkaz: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Pokud jste tento požadavek nezačali, ignorujte tento e-mail. \ No newline at end of file diff --git a/emails/translations/account-reset_da.html b/emails/translations/account-reset_da.html deleted file mode 100644 index fbe49e3693..0000000000 --- a/emails/translations/account-reset_da.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Konto reset
-
- - - - -
- [[[SERVERNAME]]] - Verificering -
-

Hej [[[USERNAME]], [[[SERVERNAME]]] anmoder om en nulstilling af kontoadgangskode, klik på følgende link for at fuldføre processen.

-

- Klik her for at nulstille adgangskoden til din konto. -

- Hvis du ikke startede denne anmodning, bedes du ignorere denne mail. -
\ No newline at end of file diff --git a/emails/translations/account-reset_da.txt b/emails/translations/account-reset_da.txt deleted file mode 100644 index 8acd2a3465..0000000000 --- a/emails/translations/account-reset_da.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Konto reset -Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) anmoder om en nulstilling af adgangskoden til kontoen. Gå til følgende link for at fuldføre processen: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Hvis du ikke startede denne anmodning, bedes du ignorere denne mail. \ No newline at end of file diff --git a/emails/translations/account-reset_de.html b/emails/translations/account-reset_de.html deleted file mode 100644 index 63caa2e8c9..0000000000 --- a/emails/translations/account-reset_de.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Zurücksetzen des Kontos
-
- - - - -
- [[[SERVERNAME]]] - Überprüfung -
-

Hallo [[[USERNAME]]], [[[SERVERNAME]]] Wenn Sie ein Zurücksetzen des Kontokennworts anfordern, klicken Sie auf den folgenden Link, um den Vorgang abzuschließen.

-

- Klicken Sie hier, um Ihr Kontopasswort zurückzusetzen. -

- Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte. -
\ No newline at end of file diff --git a/emails/translations/account-reset_de.txt b/emails/translations/account-reset_de.txt deleted file mode 100644 index fe4feb8623..0000000000 --- a/emails/translations/account-reset_de.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Zurücksetzen des Kontos -Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) fordert ein Zurücksetzen des Kontokennworts an. Klicken Sie auf den folgenden Link, um den Vorgang abzuschließen: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte. \ No newline at end of file diff --git a/emails/translations/account-reset_es.html b/emails/translations/account-reset_es.html deleted file mode 100644 index 688f27dbcd..0000000000 --- a/emails/translations/account-reset_es.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Restablecimiento de Cuenta
-
- - - - -
- [[[SERVERNAME]]] - Verificación -
-

Hola [[[USERNAME]]], [[[SERVERNAME]]] está solicitando restablecer la contraseña de la cuenta, haz clic en el siguiente enlace para completar el proceso.

-

- Haz clic aquí para restablecer la contraseña de tu cuenta. -

- Si tu no iniciaste este requerimiento, por favor ignora este correo. -
\ No newline at end of file diff --git a/emails/translations/account-reset_es.txt b/emails/translations/account-reset_es.txt deleted file mode 100644 index ca211dcab1..0000000000 --- a/emails/translations/account-reset_es.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Restablecimiento de Cuenta -Hola [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) está solicitando un restablecimiento de contraseña de cuenta. Navega al siguiente enlace para completar el proceso: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Si tu no iniciaste este requerimiento, por favor ignora este correo. \ No newline at end of file diff --git a/emails/translations/account-reset_fi.html b/emails/translations/account-reset_fi.html deleted file mode 100644 index 99bb08f22c..0000000000 --- a/emails/translations/account-reset_fi.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Tili nollattu
-
- - - - -
- [[[SERVERNAME]]] - Todentaminen -
-

Moi [[[USERNAME]]], [[[SERVERNAME]]] pyytää tilin salasanan palauttamista, suorita prosessi napsauttamalla seuraavaa linkkiä.

-

- Palauta tilisi salasana napsauttamalla tätä. -

- Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin. -
\ No newline at end of file diff --git a/emails/translations/account-reset_fi.txt b/emails/translations/account-reset_fi.txt deleted file mode 100644 index 99b6fe225b..0000000000 --- a/emails/translations/account-reset_fi.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Tili nollattu -Hei [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) pyytää tilin salasanan palauttamista. Seuraa linkkiä prosessin loppuun saattamiseksi: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin. \ No newline at end of file diff --git a/emails/translations/account-reset_fr.html b/emails/translations/account-reset_fr.html deleted file mode 100644 index 8cab5d350f..0000000000 --- a/emails/translations/account-reset_fr.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Réinitialisation du compte
-
- - - - -
- [[[SERVERNAME]]] - Vérification -
-

Bonjour [[[USERNAME]]], [[[SERVERNAME]]] demande une réinitialisation du mot de passe du compte, cliquez sur le lien suivant pour terminer le processus.

-

- Cliquez ici pour réinitialiser le mot de passe de votre compte. -

- Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier. -
\ No newline at end of file diff --git a/emails/translations/account-reset_fr.txt b/emails/translations/account-reset_fr.txt deleted file mode 100644 index 23467a20a9..0000000000 --- a/emails/translations/account-reset_fr.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Réinitialisation du compte -Bonjour [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) demande une réinitialisation du mot de passe du compte. Accédez au lien suivant pour terminer le processus: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier. \ No newline at end of file diff --git a/emails/translations/account-reset_hi.html b/emails/translations/account-reset_hi.html deleted file mode 100644 index 85808d60f4..0000000000 --- a/emails/translations/account-reset_hi.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - खाता रीसेट
-
- - - - -
- [[[SERVERNAME]]] - सत्यापन -
-

हाय [[[USERNAME]]], [[[SERVERNAME]]] खाता पासवर्ड रीसेट का अनुरोध कर रहा है, प्रक्रिया पूरी करने के लिए निम्न लिंक पर क्लिक करें।

-

- अपना खाता पासवर्ड रीसेट करने के लिए यहां क्लिक करें। -

- यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें। -
\ No newline at end of file diff --git a/emails/translations/account-reset_hi.txt b/emails/translations/account-reset_hi.txt deleted file mode 100644 index 9578c0d225..0000000000 --- a/emails/translations/account-reset_hi.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - खाता रीसेट -हाय [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) से खाता पासवर्ड रीसेट करने का अनुरोध किया जा रहा है। प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक को बताएं: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें। \ No newline at end of file diff --git a/emails/translations/account-reset_hu.html b/emails/translations/account-reset_hu.html deleted file mode 100644 index 4d8247d978..0000000000 --- a/emails/translations/account-reset_hu.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Fiók visszaállítás
-
- - - - -
- [[[SERVERNAME]]] - Megerősítés -
-

Tisztelt [[[USERNAME]]], [[[SERVERNAME]]] fiókhoz tartozó jelszó visszaállítási igényt kapott, kattintson a következő linkre a folyamat befejezéséhez.

-

- Kattintson ide fiókja jelszaváknak visszaállításához. -

- Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet. -
\ No newline at end of file diff --git a/emails/translations/account-reset_hu.txt b/emails/translations/account-reset_hu.txt deleted file mode 100644 index 39f76c7608..0000000000 --- a/emails/translations/account-reset_hu.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Fiók visszaállítás -Tisztelt [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) a fiók jelszavának visszaállítását kéri. A folyamat befejezéséhez lépjen a következő linkre: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet. \ No newline at end of file diff --git a/emails/translations/account-reset_it.html b/emails/translations/account-reset_it.html deleted file mode 100644 index e9c6ba934b..0000000000 --- a/emails/translations/account-reset_it.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Reimpostazione dell'account
-
- - - - -
- [[[SERVERNAME]]] - Verifica -
-

Salve [[[USERNAME]]], [[[SERVERNAME]]] sta richiedendo la reimpostazione della password dell'account, fare clic sul seguente collegamento per completare il processo.

-

- Clicca qui per reimpostare la password del tuo account. -

- Se non hai avviato questa richiesta, ignora questo messaggio. -
\ No newline at end of file diff --git a/emails/translations/account-reset_it.txt b/emails/translations/account-reset_it.txt deleted file mode 100644 index de70be98ac..0000000000 --- a/emails/translations/account-reset_it.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Reimpostazione dell'account -Salve [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) sta richiedendo la reimpostazione della password dell'account. Vai al seguente link per completare il processo: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Se non hai avviato questa richiesta, ignora questo messaggio. \ No newline at end of file diff --git a/emails/translations/account-reset_ja.html b/emails/translations/account-reset_ja.html deleted file mode 100644 index bd2ec17481..0000000000 --- a/emails/translations/account-reset_ja.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - アカウントのリセット
-
- - - - -
- [[[SERVERNAME]]] - 確認 -
-

[[[USERNAME]]]様 [[[SERVERNAME]]] がアカウントパスワードのリセットを要求している場合は、次のリンクをクリックしてプロセスを完了します。

-

- ここをクリックしてアカウントのパスワードをリセットしてください。 -

- このリクエストを開始していない場合は、このメールを無視してください。 -
\ No newline at end of file diff --git a/emails/translations/account-reset_ja.txt b/emails/translations/account-reset_ja.txt deleted file mode 100644 index 18e5701c65..0000000000 --- a/emails/translations/account-reset_ja.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - アカウントのリセット -こんにちは[[[USERNAME]]]、([[[SERVERURL]]][[[URLARGS1]]]) はアカウントパスワードのリセットをリクエストしています。プロセスを完了するには、次のリンクに移動します。 -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -このリクエストを開始していない場合は、このメールを無視してください。 \ No newline at end of file diff --git a/emails/translations/account-reset_ko.html b/emails/translations/account-reset_ko.html deleted file mode 100644 index db32681d18..0000000000 --- a/emails/translations/account-reset_ko.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - 계정 재설정
-
- - - - -
- [[[SERVERNAME]]] - 인증 -
-

안녕하세요, [[[USERNAME]]]님. [[[SERVERNAME]]] 계정 비밀번호 재설정을 요청하는 경우, 다음 링크를 클릭하여 과정을 완료하십시오.

-

- 당신의 계정 암호를 초기화하려면 여기를 클릭하십시오. -

- 이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오. -
\ No newline at end of file diff --git a/emails/translations/account-reset_ko.txt b/emails/translations/account-reset_ko.txt deleted file mode 100644 index b5b03b0c71..0000000000 --- a/emails/translations/account-reset_ko.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - 계정 재설정 -안녕하세요, [[[USERNAME]]]님. [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]])은 계정 비밀번호 초기화를 위해 요구됩니다. 다음 링크로 이동하여 과정을 완료하십시오 : -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오. \ No newline at end of file diff --git a/emails/translations/account-reset_nl.html b/emails/translations/account-reset_nl.html deleted file mode 100644 index cb4a4979f3..0000000000 --- a/emails/translations/account-reset_nl.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Account Reset
-
- - - - -
- [[[SERVERNAME]]] - Verificatie -
-

Hallo [[[USERNAME]]], [[[SERVERNAME]]] vraagt om het opnieuw instellen van het wachtwoord van een account, klik op de volgende link om het proces te voltooien.

-

- Klik hier om je wachtwoord opnieuw in te stellen. -

- Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren. -
\ No newline at end of file diff --git a/emails/translations/account-reset_nl.txt b/emails/translations/account-reset_nl.txt deleted file mode 100644 index 9480032c1f..0000000000 --- a/emails/translations/account-reset_nl.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Account Reset -Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) vraagt om het opnieuw instellen van het account wachtwoord. Ga naar de volgende link om het proces te voltooien: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren. \ No newline at end of file diff --git a/emails/translations/account-reset_pl.html b/emails/translations/account-reset_pl.html deleted file mode 100644 index 2a68f61999..0000000000 --- a/emails/translations/account-reset_pl.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Resetowanie Konta
-
- - - - -
- [[[SERVERNAME]]] - Weryfikacja -
-

Cześć [[[USERNAME]]], [[[SERVERNAME]]] prosi o zresetowanie hasła do konta, kliknij na poniższy link aby zakończyć proces.

-

- Kliknij tutaj, aby zresetować hasło do swojego konta. -

- Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email. -
\ No newline at end of file diff --git a/emails/translations/account-reset_pl.txt b/emails/translations/account-reset_pl.txt deleted file mode 100644 index d72e4e4fa8..0000000000 --- a/emails/translations/account-reset_pl.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Resetowanie Konta -Cześć [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) zwraca się z prośbą o zresetowanie hasła do konta. Kliknij poniższy link, aby zakończyć proces: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email. \ No newline at end of file diff --git a/emails/translations/account-reset_pt-br.html b/emails/translations/account-reset_pt-br.html deleted file mode 100644 index 7840865358..0000000000 --- a/emails/translations/account-reset_pt-br.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Redefinição de conta
-
- - - - -
- [[[SERVERNAME]]] - Verificação -
-

Olá [[[USERNAME]]], [[[SERVERNAME]]] está solicitando a redefinição da senha da conta, clique no link a seguir para concluir o processo.

-

- Clique aqui para redefinir a senha da sua conta. -

- Se você não iniciou esta solicitação, ignore este e-mail. -
\ No newline at end of file diff --git a/emails/translations/account-reset_pt-br.txt b/emails/translations/account-reset_pt-br.txt deleted file mode 100644 index 5fb99df8d1..0000000000 --- a/emails/translations/account-reset_pt-br.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Redefinição de conta -Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) está solicitando uma redefinição de senha de conta. Acesse o seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Se você não iniciou esta solicitação, ignore este e-mail. \ No newline at end of file diff --git a/emails/translations/account-reset_pt.html b/emails/translations/account-reset_pt.html deleted file mode 100644 index 7840865358..0000000000 --- a/emails/translations/account-reset_pt.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Redefinição de conta
-
- - - - -
- [[[SERVERNAME]]] - Verificação -
-

Olá [[[USERNAME]]], [[[SERVERNAME]]] está solicitando a redefinição da senha da conta, clique no link a seguir para concluir o processo.

-

- Clique aqui para redefinir a senha da sua conta. -

- Se você não iniciou esta solicitação, ignore este e-mail. -
\ No newline at end of file diff --git a/emails/translations/account-reset_pt.txt b/emails/translations/account-reset_pt.txt deleted file mode 100644 index c4dc402e34..0000000000 --- a/emails/translations/account-reset_pt.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Redefinição de conta -Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) está solicitando uma redefinição de senha de conta. Acesse o seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Se você não iniciou esta solicitação, ignore este e-mail. \ No newline at end of file diff --git a/emails/translations/account-reset_ru.html b/emails/translations/account-reset_ru.html deleted file mode 100644 index e6cba832bd..0000000000 --- a/emails/translations/account-reset_ru.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Сброс учетной записи
-
- - - - -
- [[[SERVERNAME]]] - Подтверждение -
-

Привет [[[USERNAME]]], [[[SERVERNAME]]] запрашивает сброс пароля учетной записи, нажмите на следующую ссылку, чтобы завершить процесс.

-

- Нажмите здесь, чтобы сбросить пароль учетной записи. -

- Если вы не инициировали этот запрос, игнорируйте это письмо. -
\ No newline at end of file diff --git a/emails/translations/account-reset_ru.txt b/emails/translations/account-reset_ru.txt deleted file mode 100644 index c61f41783f..0000000000 --- a/emails/translations/account-reset_ru.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Сброс учетной записи -Здравствуйте, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) запрашивает сброс пароля учетной записи. Для завершения процесса перейдите по следующей ссылке: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Если вы не инициировали этот запрос, игнорируйте это письмо. \ No newline at end of file diff --git a/emails/translations/account-reset_sv.html b/emails/translations/account-reset_sv.html deleted file mode 100644 index 594e6c0687..0000000000 --- a/emails/translations/account-reset_sv.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Återställ konto
-
- - - - -
- [[[SERVERNAME]]] - Verifiering -
-

Hej [[[USERNAME]], [[[SERVERNAME]]] begär återställning av kontolösenord, klicka på följande länk för att slutföra processen.

-

- Klicka här för att återställa ditt kontolösenord. -

- Om du inte initierade denna begäran, ignorerar du det här meddelandet. -
\ No newline at end of file diff --git a/emails/translations/account-reset_sv.txt b/emails/translations/account-reset_sv.txt deleted file mode 100644 index ab9c1419e7..0000000000 --- a/emails/translations/account-reset_sv.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Återställ konto -Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]) begär återställning av kontolösenord. Ta bort till följande länk för att slutföra processen: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Om du inte initierade denna begäran, ignorerar du det här meddelandet. \ No newline at end of file diff --git a/emails/translations/account-reset_tr.html b/emails/translations/account-reset_tr.html deleted file mode 100644 index 9faf6e795c..0000000000 --- a/emails/translations/account-reset_tr.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]] - Hesabı Sıfırlama
-
- - - - -
- [[[SERVERNAME]]] - Doğrulama -
-

Merhaba [[[USERNAME]]], [[[SERVERNAME]]] bir hesap şifresi sıfırlama talep ediyor, işlemi tamamlamak için aşağıdaki bağlantıya tıklayın.

-

- Hesap şifrenizi sıfırlamak için burayı tıklayın. -

- Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın. -
\ No newline at end of file diff --git a/emails/translations/account-reset_tr.txt b/emails/translations/account-reset_tr.txt deleted file mode 100644 index 1c30b082d9..0000000000 --- a/emails/translations/account-reset_tr.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]] - Hesabı Sıfırlama -Merhaba [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) bir hesap şifresi sıfırlama istiyor. İşlemi tamamlamak için aşağıdaki bağlantıya gidin: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın. \ No newline at end of file diff --git a/emails/translations/account-reset_zh-chs.html b/emails/translations/account-reset_zh-chs.html deleted file mode 100644 index 824250ddd2..0000000000 --- a/emails/translations/account-reset_zh-chs.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]]-帐户重置
-
- - - - -
- [[[SERVERNAME]]] - 验证 -
-

[[[USERNAME]],你好, [[[SERVERNAME]]] 正在要求重置帐户密码,请单击以下连结以完成该过程。

-

- 单击此处重置您的帐户密码。 -

- 如果您没有发起此请求,请忽略此邮件。 -
\ No newline at end of file diff --git a/emails/translations/account-reset_zh-chs.txt b/emails/translations/account-reset_zh-chs.txt deleted file mode 100644 index 8f1e1cb940..0000000000 --- a/emails/translations/account-reset_zh-chs.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]]-帐户重置 -[[[USERNAME]],您好:[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在请求重置帐户密码。导航至以下连结以完成该过程: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -如果您没有发起此请求,请忽略此邮件。 \ No newline at end of file diff --git a/emails/translations/account-reset_zh-cht.html b/emails/translations/account-reset_zh-cht.html deleted file mode 100644 index d31e2ad3c3..0000000000 --- a/emails/translations/account-reset_zh-cht.html +++ /dev/null @@ -1,15 +0,0 @@ -
[[[SERVERNAME]]]-帳戶重置
-
- - - - -
- [[[SERVERNAME]]] - 驗證 -
-

[[[USERNAME]],你好, [[[SERVERNAME]] 正在要求重置帳戶密碼,請單擊以下鏈結以完成該過程。

-

- 單擊此處重置你的帳戶密碼。 -

- 如果你沒有發起此請求,請不理此電郵。 -
\ No newline at end of file diff --git a/emails/translations/account-reset_zh-cht.txt b/emails/translations/account-reset_zh-cht.txt deleted file mode 100644 index 7356909ecb..0000000000 --- a/emails/translations/account-reset_zh-cht.txt +++ /dev/null @@ -1,6 +0,0 @@ -[[[SERVERNAME]]]-帳戶重置 -[[[USERNAME]],你好,[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在請求重設帳戶密碼。導航至以下鏈結以完成該過程: -~ -~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]] -~ -如果你沒有發起此請求,請不理此電郵。 \ No newline at end of file diff --git a/emails/translations/device-help_bs.html b/emails/translations/device-help_bs.html deleted file mode 100644 index 3c4ee3887c..0000000000 --- a/emails/translations/device-help_bs.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Zahtjev za pomoć
-
- - - - -
- [[[SERVERNAME]]] - Zahtjev za pomoć -
-

- uređaj "[[[NAZIV UREĐAJA]]]“ zatražio pomoć. -

-

- Korisnik: [[[IME POMOĆI]]]
- Zahtjev: [[[UPIT ZA POMOĆ]]] -

-

- kliknite ovdje za navigaciju do ovog uređaja. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_bs.txt b/emails/translations/device-help_bs.txt deleted file mode 100644 index d060d62b4e..0000000000 --- a/emails/translations/device-help_bs.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Zahtjev za pomoć uređaja -Uređaj "[[[DEVICENAME]]]" je zatražio pomoć. - -Korisnik: "[[[IME POMOĆI]]]" -Zahtjev: "[[[UPIT ZA POMOĆ]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_cs.html b/emails/translations/device-help_cs.html deleted file mode 100644 index ca6f8ed6d9..0000000000 --- a/emails/translations/device-help_cs.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" požádal o pomoc. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- klikněte zde to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_cs.txt b/emails/translations/device-help_cs.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_cs.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_da.html b/emails/translations/device-help_da.html deleted file mode 100644 index d152a334ee..0000000000 --- a/emails/translations/device-help_da.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- Klik her to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_da.txt b/emails/translations/device-help_da.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_da.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_de.html b/emails/translations/device-help_de.html deleted file mode 100644 index 7e01917a02..0000000000 --- a/emails/translations/device-help_de.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Gerät "[[[DEVICENAME]]]" requested help. -

-

- Benutzer: [[[HELPUSERNAME]]]
- Anforderung: [[[HELPREQUEST]]] -

-

- hier klicken um zu diesem Gerät zu navigieren. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_de.txt b/emails/translations/device-help_de.txt deleted file mode 100644 index 849df051c7..0000000000 --- a/emails/translations/device-help_de.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Gerät "[[[DEVICENAME]]]" hat Unterstützung angefordert. - -Benutzer: "[[[HELPUSERNAME]]]" -Anforderung: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_es.html b/emails/translations/device-help_es.html deleted file mode 100644 index beafbccfda..0000000000 --- a/emails/translations/device-help_es.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Solicitud de Ayuda
-
- - - - -
- [[[SERVERNAME]]] - Solicitud de Ayuda -
-

- Dispositivo "[[[DEVICENAME]]]" ha pedido ayuda. -

-

- Usuario: [[[HELPUSERNAME]]]
- Solicitud: [[[HELPREQUEST]]] -

-

- haz clic aquí para navegar a este dispositivo. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_es.txt b/emails/translations/device-help_es.txt deleted file mode 100644 index 3dc02cc3f1..0000000000 --- a/emails/translations/device-help_es.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Solicitud de ayuda con el Dispositivo -Dispositivo "[[[DEVICENAME]]]" solicito asistencia. - -Usuario: "[[[HELPUSERNAME]]]" -Solicitud: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_fi.html b/emails/translations/device-help_fi.html deleted file mode 100644 index 1dd46aaf67..0000000000 --- a/emails/translations/device-help_fi.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- klikkaa tästä to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_fi.txt b/emails/translations/device-help_fi.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_fi.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_fr.html b/emails/translations/device-help_fr.html deleted file mode 100644 index 745639b396..0000000000 --- a/emails/translations/device-help_fr.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Demande d'assistance
-
- - - - -
- [[[SERVERNAME]]] - Demande d'assistance -
-

- Appareil "[[[DEVICENAME]]]" assistance demandée. -

-

- Utilisateur : [[[HELPUSERNAME]]]
- Demande : [[[HELPREQUEST]]] -

-

- cliquez ici pour naviguer vers cet appareil. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_fr.txt b/emails/translations/device-help_fr.txt deleted file mode 100644 index 8c08fe7a74..0000000000 --- a/emails/translations/device-help_fr.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Demande d'assistance sur l'appareil -Une assistance est demandée par l'appareil "[[[DEVICENAME]]]" - -Utilistateur : "[[[HELPUSERNAME]]]" -Demande : "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_hi.html b/emails/translations/device-help_hi.html deleted file mode 100644 index c5c0a18ea4..0000000000 --- a/emails/translations/device-help_hi.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- यहाँ क्लिक करें to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_hi.txt b/emails/translations/device-help_hi.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_hi.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_hu.html b/emails/translations/device-help_hu.html deleted file mode 100644 index 75f8cb3298..0000000000 --- a/emails/translations/device-help_hu.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Segítség kérés
-
- - - - -
- [[[SERVERNAME]]] - Segítség kérés -
-

- Eszköz "[[[DEVICENAME]]]" segítséget kért. -

-

- Felhsználó: [[[HELPUSERNAME]]]
- Segítségkérés: [[[HELPREQUEST]]] -

-

- Kattintson ide az eszközhöz való navigáláshoz. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_hu.txt b/emails/translations/device-help_hu.txt deleted file mode 100644 index 4dcc6a948f..0000000000 --- a/emails/translations/device-help_hu.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Segítség kérés -A "[[[DEVICENAME]]]" eszköz segítséget kért. - -Felhasználó: "[[[HELPUSERNAME]]]" -Segítség kérés: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_it.html b/emails/translations/device-help_it.html deleted file mode 100644 index 812e9607d0..0000000000 --- a/emails/translations/device-help_it.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- clicca qui to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_it.txt b/emails/translations/device-help_it.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_it.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_ja.html b/emails/translations/device-help_ja.html deleted file mode 100644 index 42e39cbac3..0000000000 --- a/emails/translations/device-help_ja.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- ここをクリック to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_ja.txt b/emails/translations/device-help_ja.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_ja.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_ko.html b/emails/translations/device-help_ko.html deleted file mode 100644 index 37356e4389..0000000000 --- a/emails/translations/device-help_ko.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- 여기를 클릭하십시오. to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_ko.txt b/emails/translations/device-help_ko.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_ko.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_nl.html b/emails/translations/device-help_nl.html deleted file mode 100644 index 1f0f659683..0000000000 --- a/emails/translations/device-help_nl.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Verzoek om hulp
-
- - - - -
- [[[SERVERNAME]]] - Verzoek om hulp -
-

- Apparaat "[[[DEVICENAME]]]" vraagt om hulp. -

-

- Gebruiker: [[[HELPUSERNAME]]]
- Verzoek: [[[HELPREQUEST]]] -

-

- Klik hier naar dit apparaat navigeren. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_nl.txt b/emails/translations/device-help_nl.txt deleted file mode 100644 index 7a592968bb..0000000000 --- a/emails/translations/device-help_nl.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Een verzoek om het apparaat te helpen -Apparaat "[[[DEVICENAME]]]" heeft assistentie gevraagd - -Gebruiker: "[[[HELPUSERNAME]]]" -Verzoek: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_pl.html b/emails/translations/device-help_pl.html deleted file mode 100644 index b94b193d5d..0000000000 --- a/emails/translations/device-help_pl.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Prośba Pomocy
-
- - - - -
- [[[SERVERNAME]]] - Prośba Pomocy -
-

- Urządzenie "[[[DEVICENAME]]]" prosi o pomoc. -

-

- Użytkownik: [[[HELPUSERNAME]]]
- Prośba: [[[HELPREQUEST]]] -

-

- kliknij tutaj by nawigować do tego urządzenia. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_pl.txt b/emails/translations/device-help_pl.txt deleted file mode 100644 index 1b6b5a979e..0000000000 --- a/emails/translations/device-help_pl.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Prośba Pomocy Urządzenia -Urządzenie "[[[DEVICENAME]]]" prosiło o pomoc. - -Użytkownik: "[[[HELPUSERNAME]]]" -Prośba: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_pt-br.html b/emails/translations/device-help_pt-br.html deleted file mode 100644 index b77b579445..0000000000 --- a/emails/translations/device-help_pt-br.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- Clique aqui to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_pt-br.txt b/emails/translations/device-help_pt-br.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_pt-br.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_pt.html b/emails/translations/device-help_pt.html deleted file mode 100644 index b77b579445..0000000000 --- a/emails/translations/device-help_pt.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- Clique aqui to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_pt.txt b/emails/translations/device-help_pt.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_pt.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_ru.html b/emails/translations/device-help_ru.html deleted file mode 100644 index 19986fa873..0000000000 --- a/emails/translations/device-help_ru.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- кликните сюда to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_ru.txt b/emails/translations/device-help_ru.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_ru.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_sv.html b/emails/translations/device-help_sv.html deleted file mode 100644 index 8423ff37aa..0000000000 --- a/emails/translations/device-help_sv.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- Klicka här to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_sv.txt b/emails/translations/device-help_sv.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_sv.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_tr.html b/emails/translations/device-help_tr.html deleted file mode 100644 index 2844ba28ed..0000000000 --- a/emails/translations/device-help_tr.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- buraya Tıkla to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_tr.txt b/emails/translations/device-help_tr.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_tr.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_zh-chs.html b/emails/translations/device-help_zh-chs.html deleted file mode 100644 index 2f13132c0d..0000000000 --- a/emails/translations/device-help_zh-chs.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- 点击这里 to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_zh-chs.txt b/emails/translations/device-help_zh-chs.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_zh-chs.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-help_zh-cht.html b/emails/translations/device-help_zh-cht.html deleted file mode 100644 index 52d6e99080..0000000000 --- a/emails/translations/device-help_zh-cht.html +++ /dev/null @@ -1,20 +0,0 @@ -
[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
- - - - -
- [[[SERVERNAME]]] - Help Request -
-

- Device "[[[DEVICENAME]]]" requested help. -

-

- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]] -

-

- 點擊這裡 to navigate to this device. -

-
\ No newline at end of file diff --git a/emails/translations/device-help_zh-cht.txt b/emails/translations/device-help_zh-cht.txt deleted file mode 100644 index c2b3c09f4b..0000000000 --- a/emails/translations/device-help_zh-cht.txt +++ /dev/null @@ -1,7 +0,0 @@ -[[[SERVERNAME]]] - Device Help Request -Device "[[[DEVICENAME]]]" requested assistance. - -User: "[[[HELPUSERNAME]]]" -Request: "[[[HELPREQUEST]]]" - -[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]] \ No newline at end of file diff --git a/emails/translations/device-notify_bs.html b/emails/translations/device-notify_bs.html deleted file mode 100644 index c0b6ecd8c9..0000000000 --- a/emails/translations/device-notify_bs.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Obavještenje uređaja
-
- - - - -
- [[[SERVERNAME]]] - Obavještenje uređaja -
- -

- Sljedeći uređaji su promijenili stanje veze. -

-
- -

- Povezani uređaji: -

-

-[[[VEZE]]] -

-
- -

- Isključeni uređaji: -

-

-[[[PREKIDI]]] -

-
- -

- Da odjavite pretplatu, kliknite ovdje u roku od 1 sata od prijema ove poruke. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_bs.txt b/emails/translations/device-notify_bs.txt deleted file mode 100644 index ff3ba7e9ec..0000000000 --- a/emails/translations/device-notify_bs.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Obavještenje uređaja -~ -Sljedeći uređaji su promijenili stanje veze. -~ -~ -~ -Povezani uređaji: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Isključeni uređaji: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Da odjavite pretplatu, učitajte ovaj link u roku od 1 sata od prijema ove poruke: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_cs.html b/emails/translations/device-notify_cs.html deleted file mode 100644 index 37b8536e8a..0000000000 --- a/emails/translations/device-notify_cs.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Oznámení zařízení
-
- - - - -
- [[[SERVERNAME]]] - Oznámení zařízení -
- -

- Následující zařízení změnila svůj stav připojení. -

-
- -

- Připojená zařízení: -

-

-[[[CONNECTIONS]]] -

-
- -

- Odpojená zařízení: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Chcete-li se odhlásit, klikněte zde do 1 hodiny od obdržení této zprávy. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_cs.txt b/emails/translations/device-notify_cs.txt deleted file mode 100644 index fad0096bfa..0000000000 --- a/emails/translations/device-notify_cs.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Oznámení zařízení -~ -Následující zařízení změnila svůj stav připojení. -~ -~ -~ -Připojená zařízení: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Odpojená zařízení: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Chcete-li se odhlásit, načtěte tento odkaz do 1 hodiny od obdržení této zprávy: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_da.html b/emails/translations/device-notify_da.html deleted file mode 100644 index 419eff00c4..0000000000 --- a/emails/translations/device-notify_da.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Enheds Notifikation
-
- - - - -
- [[[SERVERNAME]]] - Enheds Notifikation -
- -

- Følgende enheder har ændret deres forbindelsestilstand. -

-
- -

- Forbundne enheder: -

-

-[[[CONNECTIONS]]] -

-
- -

- Afbrudte enheder: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- For at afmelde, Klik her inden for 1 time efter at have modtaget denne besked. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_da.txt b/emails/translations/device-notify_da.txt deleted file mode 100644 index 2ca27e1938..0000000000 --- a/emails/translations/device-notify_da.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Enheds Notifikation -~ -Følgende enheder har ændret deres forbindelsestilstand. -~ -~ -~ -Forbundne enheder: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Afbrudte enheder: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -For at afmelde, skal du indlæse dette link inden for 1 time efter at have modtaget denne besked: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_de.html b/emails/translations/device-notify_de.html deleted file mode 100644 index 7b97f8c2f5..0000000000 --- a/emails/translations/device-notify_de.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] – Gerätebenachrichtigung
-
- - - - -
- [[[SERVERNAME]]] – Gerätebenachrichtigung -
- -

- Die folgenden Geräte haben ihren Verbindungsstatus geändert. -

-
- -

- Verbundene Geräte: -

-

-[[[CONNECTIONS]]] -

-
- -

- Getrennte Geräte: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Deabonnieren, hier klicken innerhalb 1 Stunde nach erhalt dieser Nachricht. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_de.txt b/emails/translations/device-notify_de.txt deleted file mode 100644 index 282010179d..0000000000 --- a/emails/translations/device-notify_de.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] – Gerätebenachrichtigung -~ -Die folgenden Geräte haben ihren Verbindungsstatus geändert. -~ -~ -~ -Verbundene Geräte: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Getrennte Geräte: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Um sich abzumelden, laden Sie diesen Link innerhalb von 1 Stunde nach Erhalt dieser Nachricht: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_es.html b/emails/translations/device-notify_es.html deleted file mode 100644 index 171ec64d16..0000000000 --- a/emails/translations/device-notify_es.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Notificación del dispositivo
-
- - - - -
- [[[SERVERNAME]]] - Notificación del dispositivo -
- -

- Los siguientes dispositivos han cambiado su estado de conexión. -

-
- -

- Dispositivos conectados: -

-

-[[[CONNECTIONS]]] -

-
- -

- Dispositivos desconectados: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Para darse de baja, haz clic aquí en 1 hora desde la recepción de este mensaje. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_es.txt b/emails/translations/device-notify_es.txt deleted file mode 100644 index f3ccc002ac..0000000000 --- a/emails/translations/device-notify_es.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Notificación del dispositivo -~ -Los siguientes dispositivos han cambiado su estado de conexión. -~ -~ -~ -Dispositivos conectados: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Dispositivos desconectados: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Para darse de baja, abrir este enlace antes de 1 hora desde la recepción de este mensaje: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_fi.html b/emails/translations/device-notify_fi.html deleted file mode 100644 index e32cce33af..0000000000 --- a/emails/translations/device-notify_fi.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Laitteen ilmoitus
-
- - - - -
- [[[SERVERNAME]]] - Laitteen ilmoitus -
- -

- Seuraavat laitteet ovat muuttaneet yhteystilaa. -

-
- -

- Kytketyt laitteet: -

-

-[[[CONNECTIONS]]] -

-
- -

- Irrotetut laitteet: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Lopettaa tilauksen, klikkaa tästä tunnin sisällä tämän viestin vastaanottamisesta. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_fi.txt b/emails/translations/device-notify_fi.txt deleted file mode 100644 index 80e09f6a03..0000000000 --- a/emails/translations/device-notify_fi.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Laitteen ilmoitus -~ -Seuraavat laitteet ovat muuttaneet yhteystilaa. -~ -~ -~ -Kytketyt laitteet: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Irrotetut laitteet: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Peruuta tilaus lataamalla tämä linkki tunnin sisällä tämän viestin vastaanottamisesta: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_fr.html b/emails/translations/device-notify_fr.html deleted file mode 100644 index 86cbb4d426..0000000000 --- a/emails/translations/device-notify_fr.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Notification de l'appareil
-
- - - - -
- [[[SERVERNAME]]] - Notification de l'appareil -
- -

- Ces appareils ont changé leur statut de connexion : -

-
- -

- Appareils connectés : -

-

-[[[CONNECTIONS]]] -

-
- -

- Appareils déconnectés : -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Pour se désinscrire, cliquez ici dans l'heure suivant la réception de ce message. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_fr.txt b/emails/translations/device-notify_fr.txt deleted file mode 100644 index 7ff94a0bfb..0000000000 --- a/emails/translations/device-notify_fr.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Notification de l'appareil -~ -Ces appareils ont changé leur statut de connexion : -~ -~ -~ -Appareils connectés : -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Appareils déconnectés : -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Pour se désinscrire, chargez le lien suivant (valable une heure à réception de ce message) : [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_hi.html b/emails/translations/device-notify_hi.html deleted file mode 100644 index 255658611a..0000000000 --- a/emails/translations/device-notify_hi.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - डिवाइस नोटिफिकेशन
-
- - - - -
- [[[SERVERNAME]]] - डिवाइस नोटिफिकेशन -
- -

- निम्न उपकरणों ने अपनी कनेक्शन स्थिति बदल दी है। -

-
- -

- जुड़ी हुई डिवाइसेज: -

-

-[[[CONNECTIONS]]] -

-
- -

- डिस्कनेक्ट किए गए डिवाइस: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- सदस्यता रद्द करने के लिए, यहाँ क्लिक करें यह संदेश मिलने के 1 घंटे के भीतर। -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_hi.txt b/emails/translations/device-notify_hi.txt deleted file mode 100644 index 949ab1fb73..0000000000 --- a/emails/translations/device-notify_hi.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - डिवाइस नोटिफिकेशन -~ -निम्न उपकरणों ने अपनी कनेक्शन स्थिति बदल दी है। -~ -~ -~ -जुड़ी हुई डिवाइसेज: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -डिस्कनेक्ट किए गए डिवाइस: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -सदस्यता समाप्त करने के लिए, यह संदेश मिलने के 1 घंटे के भीतर इस लिंक को लोड करें: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_hu.html b/emails/translations/device-notify_hu.html deleted file mode 100644 index 556ec2244f..0000000000 --- a/emails/translations/device-notify_hu.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Eszköz értesítés
-
- - - - -
- [[[SERVERNAME]]] - Eszköz értesítés -
- -

- A következő eszközök kapcsolati állapota megváltozott. -

-
- -

- Csatlakozott eszközök: -

-

-[[[CONNECTIONS]]] -

-
- -

- Lecsatlakozott eszközök: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- A leiratkozáshoz, Kattintson ide az üzenet kézhezvételétől számított 1 órán belül. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_hu.txt b/emails/translations/device-notify_hu.txt deleted file mode 100644 index d025d6a05b..0000000000 --- a/emails/translations/device-notify_hu.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Eszköz értesítés -~ -A következő eszközök kapcsolati állapota megváltozott. -~ -~ -~ -Csatlakozott eszközök: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Lecsatlakozott eszközök: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -A leiratkozáshoz lépjen erre a linkre az üzenet kézhezvételétől számított 1 órán belül: [[[SERVERURL]]][[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_it.html b/emails/translations/device-notify_it.html deleted file mode 100644 index d182ab6f87..0000000000 --- a/emails/translations/device-notify_it.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Notifica dispositivo
-
- - - - -
- [[[SERVERNAME]]] - Notifica dispositivo -
- -

- I seguenti dispositivi hanno cambiato il loro stato di connessione. -

-
- -

- Dispositivi collegati: -

-

-[[[CONNECTIONS]]] -

-
- -

- Dispositivi disconnessi: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Per annullare l'iscrizione, clicca qui entro 1 ora dalla ricezione di questo messaggio. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_it.txt b/emails/translations/device-notify_it.txt deleted file mode 100644 index d8a15d3cca..0000000000 --- a/emails/translations/device-notify_it.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Notifica dispositivo -~ -I seguenti dispositivi hanno cambiato il loro stato di connessione. -~ -~ -~ -Dispositivi collegati: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Dispositivi disconnessi: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Per annullare l'iscrizione, carica questo link entro 1 ora dalla ricezione di questo messaggio: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_ja.html b/emails/translations/device-notify_ja.html deleted file mode 100644 index ae29f65425..0000000000 --- a/emails/translations/device-notify_ja.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]]-デバイス通知
-
- - - - -
- [[[SERVERNAME]]]-デバイス通知 -
- -

- 次のデバイスは接続状態を変更しました。 -

-
- -

- 接続されているデバイス: -

-

-[[[CONNECTIONS]]] -

-
- -

- 切断されたデバイス: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- 退会するには、 ここをクリック このメッセージを受け取ってから1時間以内。 -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_ja.txt b/emails/translations/device-notify_ja.txt deleted file mode 100644 index f6c794551f..0000000000 --- a/emails/translations/device-notify_ja.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]]-デバイス通知 -~ -次のデバイスは接続状態を変更しました。 -~ -~ -~ -接続されているデバイス: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -切断されたデバイス: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -登録を解除するには、次のメッセージが表示されてから1時間以内に次のリンクを読み込みます:[[[SERVERURL]]] [[[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_ko.html b/emails/translations/device-notify_ko.html deleted file mode 100644 index e397277ca2..0000000000 --- a/emails/translations/device-notify_ko.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - 기기 알림
-
- - - - -
- [[[SERVERNAME]]] - 기기 알림 -
- -

- 다음 장치의 연결 상태가 변경되었습니다. -

-
- -

- 연결된 장치: -

-

-[[[CONNECTIONS]]] -

-
- -

- 연결 해제된 장치: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- 구독을 취소하려면 여기를 클릭하십시오. 이 메시지를 받은 후 1시간 이내에 -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_ko.txt b/emails/translations/device-notify_ko.txt deleted file mode 100644 index ebcba9b46c..0000000000 --- a/emails/translations/device-notify_ko.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - 기기 알림 -~ -다음 장치의 연결 상태가 변경되었습니다. -~ -~ -~ -연결된 장치: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -연결 해제된 장치: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -구독을 취소하려면 다음 메시지를 받은 후 1시간 이내에 다음 링크를 로드하세요. [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_nl.html b/emails/translations/device-notify_nl.html deleted file mode 100644 index 7257df3530..0000000000 --- a/emails/translations/device-notify_nl.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Apparaatmelding
-
- - - - -
- [[[SERVERNAME]]] - Apparaatmelding -
- -

- De volgende apparaten hebben hun verbindingsstatus gewijzigd. -

-
- -

- Verbonden apparaten: -

-

-[[[CONNECTIONS]]] -

-
- -

- Losgekoppelde apparaten: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Uitschrijven, Klik hier binnen 1 uur na ontvangst van dit bericht. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_nl.txt b/emails/translations/device-notify_nl.txt deleted file mode 100644 index b0c3463930..0000000000 --- a/emails/translations/device-notify_nl.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Apparaatmelding -~ -De volgende apparaten hebben hun verbindingsstatus gewijzigd. -~ -~ -~ -Verbonden apparaten: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Losgekoppelde apparaten: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Om je af te melden, open je deze link binnen 1 uur nadat je dit bericht hebt ontvangen: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_pl.html b/emails/translations/device-notify_pl.html deleted file mode 100644 index 8425ff0263..0000000000 --- a/emails/translations/device-notify_pl.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Powiadomienia Urządzenia
-
- - - - -
- [[[SERVERNAME]]] - Powiadomienia Urządzenia -
- -

- Przedstawione urządzenia zmieniły stan połączenia. -

-
- -

- Podłączone urządzenia: -

-

-[[[CONNECTIONS]]] -

-
- -

- Rozłączone urządzenia: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- By się wypisać. kliknij tutaj w ciągu 1 godziny od otrzymania wiadomości. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_pl.txt b/emails/translations/device-notify_pl.txt deleted file mode 100644 index 17c09f830a..0000000000 --- a/emails/translations/device-notify_pl.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Powiadomienia Urządzenia -~ -Przedstawione urządzenia zmieniły stan połączenia. -~ -~ -~ -Podłączone urządzenia: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Rozłączone urządzenia: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -By się wypisać, załaduj ten link w ciągu 1 godziny od otrzymania wiadomości: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_pt-br.html b/emails/translations/device-notify_pt-br.html deleted file mode 100644 index 15af27c69e..0000000000 --- a/emails/translations/device-notify_pt-br.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Notificação do dispositivo
-
- - - - -
- [[[SERVERNAME]]] - Notificação do dispositivo -
- -

- Os seguintes dispositivos mudaram seus estados de conexão. -

-
- -

- Dispositivos conectados -

-

-[[[CONNECTIONS]]] -

-
- -

- Dispositivos desconectados -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Cancelar inscrição Clique aqui Com 1 hora após recebida está mensagem. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_pt-br.txt b/emails/translations/device-notify_pt-br.txt deleted file mode 100644 index b9ab12b5db..0000000000 --- a/emails/translations/device-notify_pt-br.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Notificação do dispositivo -~ -Os seguintes dispositivos mudaram seus estados de conexão. -~ -~ -~ -Dispositivos conectados -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Dispositivos desconectados -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Para cancelar inscrição, clique no link dentro de 1 hora após receber esta mensagem: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_pt.html b/emails/translations/device-notify_pt.html deleted file mode 100644 index 0e50ce6aa7..0000000000 --- a/emails/translations/device-notify_pt.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Notificação do dispositivo
-
- - - - -
- [[[SERVERNAME]]] - Notificação do dispositivo -
- -

- Os seguintes dispositivos mudaram seu estado de conexão. -

-
- -

- Dispositivos conectados: -

-

-[[[CONNECTIONS]]] -

-
- -

- Dispositivos desconectados: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Para cancelar a inscrição, Clique aqui dentro de 1 hora após receber esta mensagem. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_pt.txt b/emails/translations/device-notify_pt.txt deleted file mode 100644 index 7f7675c6b7..0000000000 --- a/emails/translations/device-notify_pt.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Notificação do dispositivo -~ -Os seguintes dispositivos mudaram seu estado de conexão. -~ -~ -~ -Dispositivos conectados: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Dispositivos desconectados: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Para cancelar a inscrição, carregue este link em até 1 hora após receber esta mensagem: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_ru.html b/emails/translations/device-notify_ru.html deleted file mode 100644 index e8b8d2b398..0000000000 --- a/emails/translations/device-notify_ru.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Уведомление устройства
-
- - - - -
- [[[SERVERNAME]]] - Уведомление устройства -
- -

- Следующие устройства изменили свое состояние подключения. -

-
- -

- Подключенные устройства: -

-

-[[[CONNECTIONS]]] -

-
- -

- Отключенные устройства: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Чтобы отписаться, кликните сюда в течение 1 часа после получения этого сообщения. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_ru.txt b/emails/translations/device-notify_ru.txt deleted file mode 100644 index 139e9144b8..0000000000 --- a/emails/translations/device-notify_ru.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Уведомление устройства -~ -Следующие устройства изменили свое состояние подключения. -~ -~ -~ -Подключенные устройства: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Отключенные устройства: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Чтобы отказаться от подписки, откройте эту ссылку в течение 1 часа после получения этого сообщения: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_sv.html b/emails/translations/device-notify_sv.html deleted file mode 100644 index a9c457d67b..0000000000 --- a/emails/translations/device-notify_sv.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] – enhetsavisering
-
- - - - -
- [[[SERVERNAME]]] – enhetsavisering -
- -

- Följande enheter har ändrat anslutningsstatus. -

-
- -

- Anslutna enheter: -

-

-[[[CONNECTIONS]]] -

-
- -

- Frånkopplade enheter: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Att avprenumerera, Klicka här inom 1 timme efter att du fick detta meddelande. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_sv.txt b/emails/translations/device-notify_sv.txt deleted file mode 100644 index be164b2b3a..0000000000 --- a/emails/translations/device-notify_sv.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] – enhetsavisering -~ -Följande enheter har ändrat anslutningsstatus. -~ -~ -~ -Anslutna enheter: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Frånkopplade enheter: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -För att avsluta prenumerationen, ladda denna länk inom 1 timme efter att du fått detta meddelande: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_tr.html b/emails/translations/device-notify_tr.html deleted file mode 100644 index 6f473e33f7..0000000000 --- a/emails/translations/device-notify_tr.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - Cihaz Bildirimi
-
- - - - -
- [[[SERVERNAME]]] - Cihaz Bildirimi -
- -

- Aşağıdaki cihazlar bağlantı durumlarını değiştirdi. -

-
- -

- Bağlı cihazlar: -

-

-[[[CONNECTIONS]]] -

-
- -

- Bağlantısı kesilen cihazlar: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- Abonelikten çıkmak, buraya Tıkla Bu mesajı aldıktan sonra 1 saat içinde. -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_tr.txt b/emails/translations/device-notify_tr.txt deleted file mode 100644 index be4553eb82..0000000000 --- a/emails/translations/device-notify_tr.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - Cihaz Bildirimi -~ -Aşağıdaki cihazlar bağlantı durumlarını değiştirdi. -~ -~ -~ -Bağlı cihazlar: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -Bağlantısı kesilen cihazlar: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -Aboneliğinizi iptal etmek için, bu mesajı aldıktan sonraki 1 saat içinde bu bağlantıyı yükleyin: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_zh-chs.html b/emails/translations/device-notify_zh-chs.html deleted file mode 100644 index c24ac8c95f..0000000000 --- a/emails/translations/device-notify_zh-chs.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - 设备通知
-
- - - - -
- [[[SERVERNAME]]] - 设备通知 -
- -

- 以下设备已更改其连接状态。 -

-
- -

- 连接的设备: -

-

-[[[CONNECTIONS]]] -

-
- -

- 断开连接的设备: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- 退订, 点击这里 在收到此消息的 1 小时内。 -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_zh-chs.txt b/emails/translations/device-notify_zh-chs.txt deleted file mode 100644 index d992e0c01d..0000000000 --- a/emails/translations/device-notify_zh-chs.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - 设备通知 -~ -以下设备已更改其连接状态。 -~ -~ -~ -连接的设备: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -断开连接的设备: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -要取消订阅,请在收到此消息后 1 小时内加载此链接:[[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/device-notify_zh-cht.html b/emails/translations/device-notify_zh-cht.html deleted file mode 100644 index 4d0ef07f65..0000000000 --- a/emails/translations/device-notify_zh-cht.html +++ /dev/null @@ -1,36 +0,0 @@ -
[[[SERVERNAME]]] - 設備通知
-
- - - - -
- [[[SERVERNAME]]] - 設備通知 -
- -

- 以下設備已更改其連接狀態。 -

-
- -

- 連接的設備: -

-

-[[[CONNECTIONS]]] -

-
- -

- 斷開連接的設備: -

-

-[[[DISCONNECTIONS]]] -

-
- -

- 退訂, 點擊這裡 在收到此消息的 1 小時內。 -

-
-
\ No newline at end of file diff --git a/emails/translations/device-notify_zh-cht.txt b/emails/translations/device-notify_zh-cht.txt deleted file mode 100644 index 22e63c0764..0000000000 --- a/emails/translations/device-notify_zh-cht.txt +++ /dev/null @@ -1,22 +0,0 @@ -[[[SERVERNAME]]] - 設備通知 -~ -以下設備已更改其連接狀態。 -~ -~ -~ -連接的設備: -~ -~[[[CONNECTIONS]]] -~ -~ -~ -~ -斷開連接的設備: -~ -~[[[DISCONNECTIONS]]] -~ -~ -~ - -要取消訂閱,請在收到此消息後 1 小時內加載此鏈接:[[[SERVERURL]]][[[UNSUBSCRIBELINK]]] -~ \ No newline at end of file diff --git a/emails/translations/mesh-invite_bs.html b/emails/translations/mesh-invite_bs.html deleted file mode 100644 index 1011318aee..0000000000 --- a/emails/translations/mesh-invite_bs.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Pozivnica
-
- - - - -
- [[[SERVERNAME]]] - Instalacija agenta -
- -

- Zdravo [[[NAME]]], -

-
-

Korisnik [[[USERNAME]]] na serveru [[[SERVERNAME]]] traži od vas da instalirate softver za pokretanje sesije daljinskog upravljanja.

- -

- Poruka: [[[MSG]]] -

-
- -

- Kliknite ovdje da preuzmete MeshAgent za Windows. -

-
- -

Kliknite ovdje da preuzmete MeshAgent za Apple OSX.

-
- -

- Za Linux, izrežite i zalijepite sljedeće u terminal da biste instalirali agenta:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Kliknite ovdje da preuzmete MeshCentral Assistant za Windows. -

-
- -

- Da biste instalirali softver, kliknite ovdje i slijedite upute. -

-
-

Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku.

- Srdačan pozdrav,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_bs.txt b/emails/translations/mesh-invite_bs.txt deleted file mode 100644 index 3080b36451..0000000000 --- a/emails/translations/mesh-invite_bs.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Pozivnica -~ -Zdravo [[[NAME]]], -~ -Korisnik [[[USERNAME]]] na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) traži da instalirate softver za pokretanje sesije daljinskog upravljanja. -~ -~ -Poruka: [[[MSG]]] -~ -~ -~ -Za Windows, idite na sljedeću vezu da dovršite proces: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Za Apple OSX, idite na sljedeću vezu da dovršite proces: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Za Linux, izrežite i zalijepite sljedeće u terminal da biste instalirali agenta: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Za MeshCentral Assistant na Windows-u, idite na sljedeću vezu da dovršite proces: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Da instalirate softver, idite na [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] i slijedite upute. -~ -Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku. -~ -Srdačan pozdrav, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_cs.html b/emails/translations/mesh-invite_cs.html deleted file mode 100644 index fd58933826..0000000000 --- a/emails/translations/mesh-invite_cs.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Pozvánka
-
- - - - -
- [[[SERVERNAME]]] - Instalace agenta -
- -

- Dobrý den, [[[NAME]]], -

-
-

Uživatel [[[USERNAME]]] na serveru [[[SERVERNAME]]] vás žádá o instalaci softwaru pro spuštění relace dálkového ovládání.

- -

- Zpráva: [[[MSG]]] -

-
- -

- Kliknutím sem stáhnete MeshAgent pro Windows. -

-
- -

Kliknutím sem stáhnete MeshAgent pro Apple OSX.

-
- -

- V případě systému Linux vyjměte a vložte do terminálu a nainstalujte agenta:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Kliknutím sem stáhnete MeshCentral Assistant pro Windows. -

-
- -

- Chcete-li nainstalovat software, klikněte zde a postupujte podle pokynů. -

-
-

Pokud jste tento požadavek nezačali, ignorujte tento e-mail.

- S pozdravem,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_cs.txt b/emails/translations/mesh-invite_cs.txt deleted file mode 100644 index a1075a5301..0000000000 --- a/emails/translations/mesh-invite_cs.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Pozvánka -~ -Dobrý den, [[[NAME]]], -~ -Uživatel [[[USERNAME]]] na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) požaduje instalaci softwaru k zahájení relace vzdáleného řízení. -~ -~ -Zpráva: [[[MSG]]] -~ -~ -~ -V případě systému Windows dokončete proces pomocí následujícího odkazu: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -V případě systému Apple OSX dokončete proces pomocí následujícího odkazu: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -V případě systému Linux vyjměte a vložte do terminálu a nainstalujte agenta: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Pro MeshCentral Assistant v systému Windows přejděte na následující odkaz a dokončete proces: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Chcete-li nainstalovat software, přejděte na [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] a postupujte podle pokynů. -~ -Pokud jste tento požadavek nezačali, ignorujte tento e-mail. -~ -S pozdravem, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_da.html b/emails/translations/mesh-invite_da.html deleted file mode 100644 index 320e962a36..0000000000 --- a/emails/translations/mesh-invite_da.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Invitation
-
- - - - -
- [[[SERVERNAME]]] - Agent installation -
- -

- Hej [[[NAME]]] -

-
-

Bruger [[[USERNAME]]] på serveren [[[SERVERNAME]]] anmoder dig om at installere software for at starte en fjernbetjeningssession.

- -

- Meddelelse: [[[MSG]]] -

-
- -

- Klik her for at downloade MeshAgent til Windows. -

-
- -

Klik her for at downloade MeshAgent til Apple OSX.

-
- -

- For Linux skal du klippe og indsætte følgende i en terminal for at installere agenten:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Klik her for at downloade MeshCentral Assistant til Windows. -

-
- -

- For at installere softwaren, Klik her og følg instruktionerne. -

-
-

Hvis du ikke startede denne anmodning, bedes du ignorere denne mail.

- Venlig hilsen,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_da.txt b/emails/translations/mesh-invite_da.txt deleted file mode 100644 index 7c96ccf8d0..0000000000 --- a/emails/translations/mesh-invite_da.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Invitation -~ -Hej [[[NAME]]] -~ -Bruger [[[USERNAME]]] på serveren [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) anmoder dig om at installere software for at starte fjernbetjeningssessionen. -~ -~ -Meddelelse: [[[[MSG]]] -~ -~ -~ -For Windows skal du navigere til følgende link for at fuldføre processen: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -For Apple OSX skal du navigere til følgende link for at fuldføre processen: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -For Linux skal du klippe og indsætte følgende i en terminal for at installere agenten: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -For MeshCentral Assistant på Windows skal du gå til følgende link for at fuldføre processen: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -For at installere softwaren skal du navigere til [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] og følge instruktionerne. -~ -Hvis du ikke startede denne anmodning, bedes du ignorere denne mail. -~ -Venlig hilsen, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_de.html b/emails/translations/mesh-invite_de.html deleted file mode 100644 index b9e61c2c7f..0000000000 --- a/emails/translations/mesh-invite_de.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Einladung
-
- - - - -
- [[[SERVERNAME]]] - Agent-Installation -
- -

- Hallo [[[NAME]]], -

-
-

Benutzer [[[USERNAME]]] auf dem Server [[[SERVERNAME]]] fordert Sie auf, Software zu installieren, um eine Fernsteuerungssitzung zu starten.

- -

- Nachricht: [[[MSG]]] -

-
- -

- Klicken Sie hier, um den MeshAgent für Windows herunterzuladen. -

-
- -

Klicken Sie hier, um den MeshAgent für Apple OSX herunterzuladen.

-
- -

- Schneiden Sie unter Linux Folgendes aus und fügen Sie es in ein Terminal ein, um den Agent zu installieren:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Klicken Sie hier, um den MeshCentral-Assistenten für Windows herunterzuladen. -

-
- -

- Um die Software zu installieren, hier klicken und folgen Sie den Anweisungen. -

-
-

Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte.

- Freundliche Grüße,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_de.txt b/emails/translations/mesh-invite_de.txt deleted file mode 100644 index 46699aa31c..0000000000 --- a/emails/translations/mesh-invite_de.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Einladung -~ -Hallo [[[NAME]]], -~ -Benutzer [[[USERNAME]]] auf dem Server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) fordert Sie auf, Software zu installieren, um die Fernsteuerungssitzung zu starten. -~ -~ -Nachricht: [[[MSG]]] -~ -~ -~ -Nagivieren Sie unter Windows den folgenden Link, um den Vorgang abzuschließen: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Nagivieren Sie unter Apple OSX den folgenden Link, um den Vorgang abzuschließen: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Schneiden Sie unter Linux Folgendes aus und fügen Sie es in ein Terminal ein, um den Agent zu installieren: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Navigieren Sie für MeshCentral Assistant unter Windows zum folgenden Link, um den Vorgang abzuschließen: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Navigieren Sie zum Installieren der Software zu [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] und befolgen Sie die Anweisungen. -~ -Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte. -~ -Freundliche Grüße, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_es.html b/emails/translations/mesh-invite_es.html deleted file mode 100644 index 3a1d396957..0000000000 --- a/emails/translations/mesh-invite_es.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Invitación
-
- - - - -
- [[[SERVERNAME]]] - Instalación de Agente -
- -

- Hola [[[NAME]]], -

-
-

Usuario [[[USERNAME]]] en servidor [[[SERVERNAME]]] te solicita que instales el software para iniciar una sesión de control remoto.

- -

- Mensaje: [[[MSG]]] -

-
- -

- Haz clic aquí para descargar el Agente Mesh para Windows. -

-
- -

Haz clic aquí para descargar el Agente Mesh para Apple OSX.

-
- -

- Para Linux, copia y pega lo siguiente en la terminal para instalar el agente:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Haga clic aquí para descargar el Asistente de MeshCentral para Windows. -

-
- -

- Para instalar el software, haz clic aquí y sigue las instrucciones. -

-
-

Si tu no iniciaste este requerimiento, por favor ignora este correo.

- Atentamente,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_es.txt b/emails/translations/mesh-invite_es.txt deleted file mode 100644 index 8ec8f17244..0000000000 --- a/emails/translations/mesh-invite_es.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Invitación -~ -Hola [[[NAME]]], -~ -Usuario [[[USERNAME]]] en servitor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) solicita que instale el software para iniciar la sesión de control remoto. -~ -~ -Mensaje: [[[MSG]]] -~ -~ -~ -Para Windows, navega al siguiente enlace para completar el proceso: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Para Apple OSX, navega hasta el siguiente enlace para completar el proceso: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Para Linux, copia y pega lo siguiente en la terminal para instalar el agente: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Para el Asistente de MeshCentral en Windows, navegue hasta el siguiente enlace para completar el proceso: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Para instalar el software, navega a [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] y siga las instrucciones. -~ -Si tu no iniciaste este requerimiento, por favor ignora este correo. -~ -Atentamente, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_fi.html b/emails/translations/mesh-invite_fi.html deleted file mode 100644 index 4e5f4862be..0000000000 --- a/emails/translations/mesh-invite_fi.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Kutsu
-
- - - - -
- [[[SERVERNAME]]] - Agentin asennus -
- -

- Hei [[[NAME]]], -

-
-

Käyttäjä [[[USERNAME]]] palvelimella [[[SERVERNAME]]] pyytää sinua asentamaan ohjelmiston etäohjausistunnon aloittamiseksi.

- -

- Viesti: [[[MSG]]] -

-
- -

- Napsauta tätä ladataksesi MeshAgent Windows:lle. -

-
- -

Napsauta tätä ladataksesi MeshAgent Apple OSX:lle.

-
- -

- Linux: leikkaa ja liitä seuraava päätelaitteeseen agentin asentamiseksi:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Napsauta tätä ladataksesi MeshCentral Assistant for Windows. -

-
- -

- Asentaaksesi ohjelman, klikkaa tästä ja seuraa ohjeita. -

-
-

Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin.

- Ystävällisin terveisin,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_fi.txt b/emails/translations/mesh-invite_fi.txt deleted file mode 100644 index c1ceef63a3..0000000000 --- a/emails/translations/mesh-invite_fi.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Kutsu -~ -Hei [[[NAME]]], -~ -Käyttäjä[[[USERNAME]]] palvelimella [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) pyytää sinua asentamaan ohjelmiston etähallintaistunnon käynnistämiseksi. -~ -~ -Viesti: [[[MSG]]] -~ -~ -~ -Jos kyseessä on Windows, suorita prosessi loppuun avaamalla seuraava linkki: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Apple OSX -käyttöjärjestelmässä prosessin loppuun saattamiseksi napsauta seuraavaa linkkiä: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Linux: leikkaa ja liitä seuraava päätelaitteeseen agentin asentamiseksi: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Jos käytät MeshCentral Assistantia Windowsissa, siirry seuraavaan linkkiin prosessin viimeistelemiseksi: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Asenna ohjelmisto siirtymällä kohtaan [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] ja noudattamalla ohjeita. -~ -Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin. -~ -Ystävällisin terveisin, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_fr.html b/emails/translations/mesh-invite_fr.html deleted file mode 100644 index 6e9f82c4d1..0000000000 --- a/emails/translations/mesh-invite_fr.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Invitation
-
- - - - -
- [[[SERVERNAME]]] - Installation de l'agent -
- -

- Bonjour [[[NAME]]], -

-
-

Utilisateur [[[USERNAME]]] sur le serveur [[[SERVERNAME]]] vous demande d'installer un logiciel pour démarrer une session de contrôle à distance.

- -

- Message: [[[MSG]]] -

-
- -

- Cliquez ici pour télécharger le MeshAgent pour Windows. -

-
- -

Cliquez ici pour télécharger le MeshAgent pour Apple OSX.

-
- -

- Pour Linux, copiez et collez les éléments suivants dans un terminal pour installer l'agent:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Cliquez ici pour télécharger l'assistant Meshcentral pour Windows. -

-
- -

- Pour installer le logiciel, cliquez ici et suivez les instructions. -

-
-

Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier.

- Meilleures salutations,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_fr.txt b/emails/translations/mesh-invite_fr.txt deleted file mode 100644 index 723e6ff86c..0000000000 --- a/emails/translations/mesh-invite_fr.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Invitation -~ -Bonjour [[[NAME]]], -~ -L'utilisateur [[[USERNAME]]] sur le serveur [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) vous demande d'installer un logiciel pour démarrer la session de contrôle à distance. -~ -~ -Message: [[[MSG]]] -~ -~ -~ -Pour Windows, accédez au lien suivant pour terminer le processus: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Pour Apple OSX, accédez au lien suivant pour terminer le processus: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Pour Linux, copiez et collez les éléments suivants dans un terminal pour installer l'agent: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Pour l'assistant MeshCentral sous Windows, cliquez sur le lien suivant pour compléter le processus : -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Pour installer le logiciel, accédez à [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] et suivez les instructions. -~ -Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier. -~ -Meilleures salutations, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_hi.html b/emails/translations/mesh-invite_hi.html deleted file mode 100644 index b59cb88cdd..0000000000 --- a/emails/translations/mesh-invite_hi.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - निमंत्रण
-
- - - - -
- [[[SERVERNAME]]] - एजेंट स्थापना -
- -

- नमस्कार [[[NAME]]], -

-
-

उपयोगकर्ता [[[USERNAME]]] सर्वर पर [[[SERVERNAME]]] रिमोट कंट्रोल सेशन शुरू करने के लिए आपको सॉफ्टवेयर इंस्टॉल करने का अनुरोध कर रहा है।

- -

- संदेश: [[[MSG]]] -

-
- -

- विंडोज के लिए मेशजेंट को डाउनलोड करने के लिए यहां क्लिक करें। -

-
- -

Apple OSX के लिए मेशजेंट को डाउनलोड करने के लिए यहां क्लिक करें।

-
- -

- लिनक्स में, एजेंट को स्थापित करने के लिए टर्मिनल में निम्नलिखित को काटें और चिपकाएँ:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- विंडोज के लिए मेशसेंट्रल असिस्टेंट डाउनलोड करने के लिए यहां क्लिक करें। -

-
- -

- सॉफ़्टवेयर स्थापित करने के लिए, यहाँ क्लिक करें और निर्देशों का पालन करें। -

-
-

यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें।

- सादर,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_hi.txt b/emails/translations/mesh-invite_hi.txt deleted file mode 100644 index 70c2899f9b..0000000000 --- a/emails/translations/mesh-invite_hi.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - निमंत्रण -~ -नमस्कार [[[NAME]]], -~ -उपयोगकर्ता [[[USERNAME]]] सर्वर पर [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) आपसे रिमोट कंट्रोल सेशन शुरू करने के लिए सॉफ़्टवेयर स्थापित करने का अनुरोध कर रहा है। -~ -~ -संदेश: [[[MSG]]] -~ -~ -~ -विंडोज के लिए, प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक पर जाएं: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Apple OSX के लिए, इस प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक पर जाएं: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -लिनक्स में, एजेंट को स्थापित करने के लिए टर्मिनल में निम्नलिखित को काटें और चिपकाएँ: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -विंडोज़ पर मेशसेंट्रल असिस्टेंट के लिए, प्रक्रिया को पूरा करने के लिए निम्न लिंक पर जाएँ: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -सॉफ़्टवेयर को स्थापित करने के लिए, [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] पर जाएँ और निर्देशों का पालन करें। -~ -यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें। -~ -सादर, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_hu.html b/emails/translations/mesh-invite_hu.html deleted file mode 100644 index 00c299361a..0000000000 --- a/emails/translations/mesh-invite_hu.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Meghívás
-
- - - - -
- [[[SERVERNAME]]] - Agent Telepítése -
- -

- Tisztelt [[[NAME]]], -

-
-

[[[USERNAME]]] felhasználó, kiszolgáló: [[[SERVERNAME]]] kéri hogy telepítse az alkalmazást, amivel lehetőség van távvezérlő munkamant elindítására.

- -

- Üzenet: [[[MSG]]] -

-
- -

- Kattintson ide a MeshAgent for Windows letöltéséhez. -

-
- -

Kattintson ide az Apple OSX rendszerhez készült MeshAgent letöltéséhez.

-
- -

- Linux esetén az Agent telepítéséhez vágja ki és illessze be egy terminálba a következőt:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Kattintson ide a MeshCentral Assistant for Windows letöltéséhez. -

-
- -

- A szoftver telepítéséhez, Kattintson ide és kövesse az utasításokat. -

-
-

Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet.

- Üdvözlettel,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_hu.txt b/emails/translations/mesh-invite_hu.txt deleted file mode 100644 index a73f47465c..0000000000 --- a/emails/translations/mesh-invite_hu.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Meghívás -~ -Tisztelt [[[NAME]]], -~ -A [[[USERNAME]]] felhasználó a [[[SERVERNAME]]] kiszolgálón ([[[[SERVERURL]]]/[[[URLARGS1]]]) a távvezérlési munkamenet indításához szükséges szoftver telepítését kéri. -~ -~ -Üzenet: [[[MSG]]] -~ -~ -~ -Windows esetén a folyamat befejezéséhez menjen a következő hivatkozásra: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Apple OSX esetén kattintson az alábbi linkre a folyamat befejezéséhez: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Linux esetén az Agent telepítéséhez vágja ki és illessze be egy terminálba a következőt: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Windows rendszeren futó MeshCentral Assistant esetén menjen a következő hivatkozásra a folyamat befejezéséhez: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -A szoftver telepítéséhez kérjük lépjen a [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] oldalra, és kövesse az utasításokat. -~ -Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet. -~ -Üdvözlettel, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_it.html b/emails/translations/mesh-invite_it.html deleted file mode 100644 index 3e767207ed..0000000000 --- a/emails/translations/mesh-invite_it.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Invito
-
- - - - -
- [[[SERVERNAME]]] - Installazione dell'agente -
- -

- Salve [[[NAME]]], -

-
-

Utente [[[USERNAME]]] sul server [[[SERVERNAME]]] chiede di installare un software per avviare una sessione di controllo remoto.

- -

- Messaggio: [[[MSG]]] -

-
- -

- Fare clic qui per scaricare MeshAgent per Windows. -

-
- -

Fare clic qui per scaricare MeshAgent per Apple OSX.

-
- -

- Per Linux, taglia e incolla quanto segue in un terminale per installare l'agente:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Fare clic qui per scaricare MeshCentral Assistant per Windows. -

-
- -

- Per installare il software, clicca qui e segui le istruzioni. -

-
-

Se non hai avviato questa richiesta, ignora questo messaggio.

- I migliori saluti,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_it.txt b/emails/translations/mesh-invite_it.txt deleted file mode 100644 index c008c7808f..0000000000 --- a/emails/translations/mesh-invite_it.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Invito -~ -Salve [[[NAME]]], -~ -L'utente [[[USERNAME]]] sul server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) sta richiedendo l'installazione del software per avviare la sessione di controllo remoto. -~ -~ -Messaggio: [[[MSG]]] -~ -~ -~ -Per Windows, accedi al seguente collegamento per completare il processo: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Per Apple OSX, vai al seguente link per completare il processo: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Per Linux, taglia e incolla quanto segue in un terminale per installare l'agente: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Per MeshCentral Assistant su Windows, accedere al seguente collegamento per completare il processo: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Per installare il software, vai a [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] e segui le istruzioni. -~ -Se non hai avviato questa richiesta, ignora questo messaggio. -~ -I migliori saluti, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_ja.html b/emails/translations/mesh-invite_ja.html deleted file mode 100644 index 3cd59ed9a3..0000000000 --- a/emails/translations/mesh-invite_ja.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - 招待
-
- - - - -
- [[[SERVERNAME]]] - エージェントのインストール -
- -

- [[[NAME]]]様 -

-
-

サーバー上のユーザー[[[USERNAME]]] [[[SERVERNAME]]] リモートコントロールセッションを開始するソフトウェアをインストールするように要求しています。

- -

- メッセージ: [[[MSG]]] -

-
- -

- Windows用のMeshAgentをダウンロードするには、ここをクリックしてください。 -

-
- -

ここをクリックして、Apple OSX用のMeshAgentをダウンロードしてください。

-
- -

- Linuxの場合は、ターミナルで以下をカットアンドペーストしてエージェントをインストールします。
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Windows用のMeshCentral-Assistantをダウンロードするには、ここをクリックしてください。 -

-
- -

- ソフトウェアをインストールするには、 ここをクリック 指示に従ってください。 -

-
-

このリクエストを開始していない場合は、このメールを無視してください。

- 宜しくお願いします、
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_ja.txt b/emails/translations/mesh-invite_ja.txt deleted file mode 100644 index 248a316d05..0000000000 --- a/emails/translations/mesh-invite_ja.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - 招待 -~ -[[[NAME]]]様 -~ -サーバー[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) のユーザー[[[USERNAME]]]が、リモートコントロールセッションを開始するためのソフトウェアのインストールを要求しています。 -~ -~ -メッセージ:[[[MSG]]] -~ -~ -~ -Windowsの場合、次のリンクに移動してプロセスを完了します。 -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Apple OSXの場合、次のリンクに移動してプロセスを完了します。 -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Linuxの場合は、ターミナルで以下をカットアンドペーストしてエージェントをインストールします。 -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -WindowsのMeshCentral Assistantの場合、次のリンクに移動してプロセスを完了します。 -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -ソフトウェアをインストールするには、[[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]]に移動し、指示に従います。 -~ -このリクエストを開始していない場合は、このメールを無視してください。 -~ -宜しくお願いします、 -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_ko.html b/emails/translations/mesh-invite_ko.html deleted file mode 100644 index a01a297b2a..0000000000 --- a/emails/translations/mesh-invite_ko.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - 초대
-
- - - - -
- [[[SERVERNAME]]] - 에이전트 설치 -
- -

- 안녕하세요, [[[NAME]]]님. -

-
-

서버의 [[[USERNAME]]] 사용자 [[[SERVERNAME]]] 원격 제어 세션을 시작하기 위해 소프트웨어 설치를 요청하고 있습니다.

- -

- 메시지: [[[MSG]]] -

-
- -

- Windows를 위한 MeshAgent를 다운로드 하려면 여기를 클릭하십시오. -

-
- -

Apple OSX를 위한 MeshAgent를 다운로드 하려면 여기를 클릭하십시오.

-
- -

- Linux의 경우, 다음을 잘라내어 터미널에 붙여 넣어 에이전트를 설치하십시오.
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Windows용 MeshCentral Assistant를 다운로드하려면 여기를 클릭하십시오. -

-
- -

- 소프트웨어를 설치하려면, 여기를 클릭하십시오. 그리고 다음 지시들을 따르십시오. -

-
-

이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오.

- 최고의 안부를 전합니다,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_ko.txt b/emails/translations/mesh-invite_ko.txt deleted file mode 100644 index 35175f80fe..0000000000 --- a/emails/translations/mesh-invite_ko.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - 초대 -~ -안녕하세요, [[[NAME]]]님. -~ -[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) 서버의 [[[USERNAME]]] 사용자가 원격 제어 세션을 시작하기 위해서는 소프트웨어 설치가 요구됩니다. -~ -~ -메시지: [[[MSG]]] -~ -~ -~ -Windows의 경우, 다음 링크로 이동하여 과정을 완료하십시오. -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Apple OSX인 경우, 다음 링크로 이동하여 과정을 완료하십시오. -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Linux의 경우, 다음을 잘라내어 터미널에 붙여 넣어 에이전트를 설치하십시오. -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Windows용 MeshCentral Assistant의 경우 다음 링크로 이동하여 프로세스를 완료하십시오. -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -소프트웨어를 설치하려면, 다음에 접속하여 지시에 따르십시오 : [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] -~ -이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오. -~ -최고의 안부를 전합니다, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_nl.html b/emails/translations/mesh-invite_nl.html deleted file mode 100644 index 0cbfa53d87..0000000000 --- a/emails/translations/mesh-invite_nl.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Uitnodiging
-
- - - - -
- [[[SERVERNAME]]] - Agent installatie -
- -

- Hallo [[[NAME]]], -

-
-

Gebruiker [[[USERNAME]]] op server [[[SERVERNAME]]] vraagt u om de software te installeren om een ondersteunings sessie te starten.

- -

- Bericht: [[[MSG]]] -

-
- -

- Klik hier om de MeshAgent te downloaden voor Windows. -

-
- -

Klik hier om de MeshAgent te downloaden voor Apple OSX.

-
- -

- Voor Linux, knip het volgende en plak dit in een terminal om de agent te installeren:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Klik hier om de MeshCentral Assistant voor Windows te downloaden. -

-
- -

- Om de software te installeren, Klik hier en volg de instructies. -

-
-

Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren.

- Vriendelijke groeten,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_nl.txt b/emails/translations/mesh-invite_nl.txt deleted file mode 100644 index dbaebaddd1..0000000000 --- a/emails/translations/mesh-invite_nl.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Uitnodiging -~ -Hallo [[[NAME]]], -~ -Gebruiker [[[USERNAME]]] op server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) vraagt u de software te installeren om de sessie voor afstandsbediening te starten. -~ -~ -Bericht: [[[MSG]]] -~ -~ -~ -Voor Windows, ga je naar de volgende link om het proces te voltooien: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Voor Apple OSX ga je naar de volgende link om het proces te voltooien: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Voor Linux, knip het volgende en plak dit in een terminal om de agent te installeren: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Voor MeshCentral Assistant voor Windows navigeert u naar de volgende link om het proces te voltooien: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Om de software te installeren, navigeert u naar [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] en volgt u de instructies. -~ -Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren. -~ -Vriendelijke groeten, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_pl.html b/emails/translations/mesh-invite_pl.html deleted file mode 100644 index 35f3e02abf..0000000000 --- a/emails/translations/mesh-invite_pl.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Zaproszenie
-
- - - - -
- [[[SERVERNAME]]] - Instalacja Agenta -
- -

- Witaj [[[NAME]]], -

-
-

Użytkownik [[[USERNAME]]] na serwerze [[[SERVERNAME]]] prosi o zainstalowanie oprogramowania w celu rozpoczęcia sesji zdalnego sterowania.

- -

- Wiadomość: [[[MSG]]] -

-
- -

- Kliknij tutaj aby pobrać MeshAgent dla Windows. -

-
- -

Kliknij tutaj aby pobrać MeshAgent dla Apple OSX.

-
- -

- W systemie Linuks, wytnij i wklej poniższe polecenia w terminalu, aby zainstalować agenta:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Kliknij tutaj by pobrać Asystenta MeshCentral dla Windows. -

-
- -

- Aby zainstalować oprogramowanie, kliknij tutaj i postępuj zgodnie z instrukcjami. -

-
-

Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email.

- Z wyrazami szacunku,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_pl.txt b/emails/translations/mesh-invite_pl.txt deleted file mode 100644 index 4eef498476..0000000000 --- a/emails/translations/mesh-invite_pl.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Zaproszenie -~ -Witaj [[[NAME]]], -~ -Użytkownik [[[USERNAME]]] na serwerze [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) prosi o zainstalowanie oprogramowania w celu uruchomienia sesji zdalnego sterowania. -~ -~ -Wiadomość: [[[MSG]]] -~ -~ -~ -W przypadku systemu Windows należy przejść do następującego linku, aby zakończyć proces: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Dla Apple OSX, przejdź do następującego linku, aby zakończyć proces: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -W systemie Linuks, wytnij i wklej poniższe polecenia w terminalu, aby zainstalować agenta: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -W przypadku Asystenta MeshCentral w Windows, przejdź do tego linku aby zakończyć proces: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Aby zainstalować oprogramowanie, przejdź do [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] i postępuj zgodnie z instrukcjami. -~ -Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email. -~ -Z wyrazami szacunku, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_pt-br.html b/emails/translations/mesh-invite_pt-br.html deleted file mode 100644 index 00fc013bde..0000000000 --- a/emails/translations/mesh-invite_pt-br.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Convite
-
- - - - -
- [[[SERVERNAME]]] - Instalação do Agente -
- -

- Olá [[[NAME]]], -

-
-

Usuário [[[USERNAME]]] no servidor [[[SERVERNAME]]] está solicitando a instalação de um software para iniciar uma sessão de controle remoto.

- -

- Mensagem: [[[MSG]]] -

-
- -

- Clique aqui para baixar o MeshAgent para Windows. -

-
- -

Clique aqui para baixar o MeshAgent para Apple OSX.

-
- -

- Para Linux, recorte e cole o seguinte em um terminal para instalar o agente:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Clique aqui para baixar o Mesh Central Assistant para Windows -

-
- -

- Para instalar o software, Clique aqui e siga as instruções. -

-
-

Se você não iniciou esta solicitação, ignore este e-mail.

- Atenciosamente,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_pt-br.txt b/emails/translations/mesh-invite_pt-br.txt deleted file mode 100644 index 83fe1d1dea..0000000000 --- a/emails/translations/mesh-invite_pt-br.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Convite -~ -Olá [[[NAME]]], -~ -O usuário [[[USERNAME]]] no servidor [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]]) está solicitando a instalação do software para iniciar a sessão de controle remoto. -~ -~ -Mensagem: [[[MSG]]] -~ -~ -~ -Para Windows, vá ao seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Para Apple OSX, vá ao seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Para Linux, recorte e cole o seguinte em um terminal para instalar o agente: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Para o MeshCentral Assistant no Windows, abre o seguinte link para completar o processo: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Para instalar o software, navegue até [[[SERVERURL]]] [[[LINKURL]]] [[[URLARGS2]]] e siga as instruções. -~ -Se você não iniciou esta solicitação, ignore este e-mail. -~ -Atenciosamente, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_pt.html b/emails/translations/mesh-invite_pt.html deleted file mode 100644 index 71603863f3..0000000000 --- a/emails/translations/mesh-invite_pt.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Convite
-
- - - - -
- [[[SERVERNAME]]] - Instalação do Agente -
- -

- Olá [[[NAME]]], -

-
-

Usuário [[[USERNAME]]] no servidor [[[SERVERNAME]]] está solicitando a instalação de um software para iniciar uma sessão de controle remoto.

- -

- Mensagem: [[[MSG]]] -

-
- -

- Clique aqui para baixar o MeshAgent para Windows. -

-
- -

Clique aqui para baixar o MeshAgent para Apple OSX.

-
- -

- Para Linux, recorte e cole o seguinte em um terminal para instalar o agente:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Clique aqui para baixar o Assistente MeshCentral para Windows. -

-
- -

- Para instalar o software, Clique aqui e siga as instruções. -

-
-

Se você não iniciou esta solicitação, ignore este e-mail.

- Cumprimentos,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_pt.txt b/emails/translations/mesh-invite_pt.txt deleted file mode 100644 index 945095e3e3..0000000000 --- a/emails/translations/mesh-invite_pt.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Convite -~ -Olá [[[NAME]]], -~ -O usuário [[[USERNAME]]] no servidor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) está solicitando a instalação do software para iniciar a sessão de controle remoto. -~ -~ -Mensagem: [[[MSG]]] -~ -~ -~ -Para Windows, vá ao seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Para Apple OSX, vá ao seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Para Linux, recorte e cole o seguinte em um terminal para instalar o agente: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Para o MeshCentral Assistant no Windows, navegue até o seguinte link para concluir o processo: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Para instalar o software, navegue até [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] e siga as instruções. -~ -Se você não iniciou esta solicitação, ignore este e-mail. -~ -Cumprimentos, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_ru.html b/emails/translations/mesh-invite_ru.html deleted file mode 100644 index 8e90fbc2c2..0000000000 --- a/emails/translations/mesh-invite_ru.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Приглашение
-
- - - - -
- [[[SERVERNAME]]] - Установка агента -
- -

- Здравствуйте, [[[NAME]]], -

-
-

Пользователь [[[USERNAME]]] на сервере [[[SERVERNAME]]] просит вас установить программное обеспечение, чтобы начать сеанс удаленного управления.

- -

- Сообщение: [[[MSG]]] -

-
- -

- Нажмите здесь, чтобы загрузить MeshAgent для Windows. -

-
- -

Нажмите здесь, чтобы загрузить MeshAgent для Apple OSX.

-
- -

- Для Linux вырезайте и вставляйте в терминал следующее, чтобы установить агент:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Нажмите здесь, чтобы загрузить MeshCentral Assistant для Windows. -

-
- -

- Чтобы установить программное обеспечение, кликните сюда и следуйте инструкциям. -

-
-

Если вы не инициировали этот запрос, игнорируйте это письмо.

- С уважением,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_ru.txt b/emails/translations/mesh-invite_ru.txt deleted file mode 100644 index db629717c9..0000000000 --- a/emails/translations/mesh-invite_ru.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Приглашение -~ -Здравствуйте, [[[NAME]]], -~ -Пользователь [[[USERNAME]]] на сервере [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) запрашивает установку программного обеспечения для запуска сеанса удаленного управления. -~ -~ -Сообщение: [[[MSG]]] -~ -~ -~ -Для Windows откройте следующую ссылку, чтобы завершить процесс: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Для Apple OSX перейдите по следующей ссылке, чтобы завершить процесс: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Для Linux вырезайте и вставляйте в терминал следующее, чтобы установить агент: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Для MeshCentral Assistant под Windows перейдите по следующей ссылке, чтобы завершить процесс: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Для установки программного обеспечения перейдите к [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] и следуйте инструкциям. -~ -Если вы не инициировали этот запрос, игнорируйте это письмо. -~ -С уважением, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_sv.html b/emails/translations/mesh-invite_sv.html deleted file mode 100644 index 478a60d1f7..0000000000 --- a/emails/translations/mesh-invite_sv.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Inbjudan
-
- - - - -
- [[[SERVERNAME]]] - Agentinstallation -
- -

- Hej [[[NAME]]] -

-
-

Användare [[[USERNAME]]] på servern [[[SERVERNAME]]] ber dig att installera programvara för att starta en fjärrkontrollsession.

- -

- Meddelande: [[[MSG]]] -

-
- -

- Klicka här för att ladda ner MeshAgent för Windows. -

-
- -

Klicka här för att ladda ner MeshAgent för Apple OSX.

-
- -

- För Linux, klipp och klistra in följande i en terminal för att installera agenten:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Klicka här för att ladda ner MeshCentral Assistant för Windows. -

-
- -

- För att installera programvaran, Klicka här och följ instruktionerna. -

-
-

Om du inte initierade denna begäran, ignorerar du det här meddelandet.

- Vänliga hälsningar,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_sv.txt b/emails/translations/mesh-invite_sv.txt deleted file mode 100644 index c32bd51b6b..0000000000 --- a/emails/translations/mesh-invite_sv.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Inbjudan -~ -Hej [[[NAME]]] -~ -Användaren [[[USERNAME]]] på servern [[[SERVERNAME]]] ([[[SERVERURL]]/[[[URLARGS1]]) ber dig installera programvara för att starta fjärrkontrollsessionen. -~ -~ -Meddelande: [[[[MSG]]] -~ -~ -~ -För Windows, nagivera till följande länk för att slutföra processen: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -För Apple OSX, nagivera till följande länk för att slutföra processen: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -För Linux, klipp och klistra in följande i en terminal för att installera agenten: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -För MeshCentral Assistant på Windows, gå till följande länk för att slutföra processen: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -För att installera programvaran, navigera till [[[SERVERURL]]] [[[LINKURL]]] [[[URLARGS2]]] och följ instruktionerna. -~ -Om du inte initierade denna begäran, ignorerar du det här meddelandet. -~ -Vänliga hälsningar, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_tr.html b/emails/translations/mesh-invite_tr.html deleted file mode 100644 index 1036a9d0ad..0000000000 --- a/emails/translations/mesh-invite_tr.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - Davet
-
- - - - -
- [[[SERVERNAME]]] - Agent Kurulumu -
- -

- Merhaba [[[NAME]]], -

-
-

Sunucudaki kullanıcı [[[USERNAME]]] [[[SERVERNAME]]] bir uzaktan kontrol oturumu başlatmak için yazılım yüklemenizi istiyor.

- -

- İleti: [[[MSG]]] -

-
- -

- Windows için MeshAgent'ı indirmek için buraya tıklayın. -

-
- -

Apple OSX için MeshAgent'ı indirmek için burayı tıklayın.

-
- -

- Linux için, aracıyı yüklemek için aşağıdakileri kesip bir terminale yapıştırın:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- Windows için MeshCentral Assistant'ı indirmek için buraya tıklayın. -

-
- -

- Yazılımı kurmak için, buraya Tıkla ve talimatları izleyin. -

-
-

Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın.

- Saygılarımla,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_tr.txt b/emails/translations/mesh-invite_tr.txt deleted file mode 100644 index df97c4bc4f..0000000000 --- a/emails/translations/mesh-invite_tr.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - Davet -~ -Merhaba [[[NAME]]], -~ -[[[SERVERNAME]]] [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) sunucusundaki [[[USERNAME]]] uzaktan kontrol oturumunu başlatmak için yazılım yüklemenizi istiyor. -~ -~ -Mesaj: [[[MSG]]] -~ -~ -~ -Windows için, işlemi tamamlamak için aşağıdaki bağlantıya gidin: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Apple OSX için, işlemi tamamlamak için aşağıdaki bağlantıya gidin: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -Linux için, aracıyı yüklemek için aşağıdakileri kesip bir terminale yapıştırın: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -Windows'ta MeshCentral Assistant için işlemi tamamlamak üzere aşağıdaki bağlantıya gidin: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -Yazılımı kurmak için [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] adresine gidin ve talimatları takip edin. -~ -Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın. -~ -Saygılarımla, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_zh-chs.html b/emails/translations/mesh-invite_zh-chs.html deleted file mode 100644 index 6c5b9412d2..0000000000 --- a/emails/translations/mesh-invite_zh-chs.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - 邀请
-
- - - - -
- [[[SERVERNAME]]]-代理程序安装 -
- -

- 您好[[[NAME]]], -

-
-

服务器上的使用者[[[USERNAME]]] [[[SERVERNAME]]] 正在要求您安装软件以启动远程控制会话。

- -

- 消息: [[[MSG]]] -

-
- -

- 单击此处下载适用于Windows的MeshAgent。 -

-
- -

单击此处下载适用于Apple OSX的MeshAgent。

-
- -

- 对于Linux,将以下内容剪切并粘贴到终端中以安装代理软件:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- 单击此处下载适用于 Windows 的 MeshCentral 助手。 -

-
- -

- 要安装软件, 点击这里 并按照说明进行操作。 -

-
-

如果您没有发起此请求,请忽略此邮件。

- 最好的祝福,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_zh-chs.txt b/emails/translations/mesh-invite_zh-chs.txt deleted file mode 100644 index 20d8af69d4..0000000000 --- a/emails/translations/mesh-invite_zh-chs.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - 邀请 -~ -您好[[[NAME]]], -~ -服务器[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) 上的用户[[[USERNAME]]]请求您安装软件以启动远程控制。 -~ -~ -消息:[[[MSG]]] -~ -~ -~ -对于Windows,请导航至以下连结以完成该过程: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -对于Apple OSX,请导航至以下连结以完成该过程: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -对于Linux,将以下内容剪切并粘贴到终端中以安装代理软件: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -对于 Windows 上的 MeshCentral Assistant,导航到以下链接以完成该过程: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -要安装软件,请导航至[[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]],然后按照说明进行操作。 -~ -如果您没有发起此请求,请忽略此邮件。 -~ -最好的祝福, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/mesh-invite_zh-cht.html b/emails/translations/mesh-invite_zh-cht.html deleted file mode 100644 index 8d6d011f60..0000000000 --- a/emails/translations/mesh-invite_zh-cht.html +++ /dev/null @@ -1,47 +0,0 @@ -
[[[SERVERNAME]]] - 邀請
-
- - - - -
- [[[SERVERNAME]]]-代理程序安裝 -
- -

- 你好[[[NAME]]], -

-
-

伺服器上的使用者[[[USERNAME]]] [[[SERVERNAME]] 正在要求你安裝軟體以啟動遠程控制會話。

- -

- 訊息: [[[MSG]]] -

-
- -

- 單擊此處下載適用於Windows的MeshAgent。 -

-
- -

單擊此處下載適用於Apple OSX的MeshAgent。

-
- -

- 對於Linux,將以下內容剪切並粘貼到終端中以安裝代理軟體:
-

wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-

-
- -

- 單擊此處下載適用於 Windows 的 MeshCentral 助手。 -

-
- -

- 要安裝軟體, 點擊這裡 並按照說明進行操作。 -

-
-

如果你沒有發起此請求,請不理此電郵。

- 最好的祝福,
[[[USERNAME]]]
-
\ No newline at end of file diff --git a/emails/translations/mesh-invite_zh-cht.txt b/emails/translations/mesh-invite_zh-cht.txt deleted file mode 100644 index 0d6ef61601..0000000000 --- a/emails/translations/mesh-invite_zh-cht.txt +++ /dev/null @@ -1,41 +0,0 @@ -[[[SERVERNAME]]] - 邀請 -~ -你好[[[NAME]]], -~ -伺服器[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) 上的用戶[[[USERNAME]]]請求你安裝軟體以啟動遠程控制。 -~ -~ -訊息:[[[MSG]]] -~ -~ -~ -對於Windows,請導航至以下鏈結以完成該過程: -~ -~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -對於Apple OSX,請導航至以下鏈結以完成該過程: -~ -~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]] -~ -~ -~ -對於Linux,將以下內容剪切並粘貼到終端中以安裝代理軟體: -~ -~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]' -~ -~ -~ -對於 Windows 上的 MeshCentral Assistant,導航到以下鏈接以完成該過程: -~ -~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]] -~ -~ -~ -要安裝軟體,請導航至[[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]],然後按照說明進行操作。 -~ -如果你沒有發起此請求,請不理此電郵。 -~ -最好的祝福, -~[[[USERNAME]]] \ No newline at end of file diff --git a/emails/translations/sms-messages_bs.txt b/emails/translations/sms-messages_bs.txt deleted file mode 100644 index 768b0043b0..0000000000 --- a/emails/translations/sms-messages_bs.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] verifikacioni kod je: [[1]] -[[0]] pristupni token je: [[1]] diff --git a/emails/translations/sms-messages_cs.txt b/emails/translations/sms-messages_cs.txt deleted file mode 100644 index c859513d42..0000000000 --- a/emails/translations/sms-messages_cs.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] ověřovací kód je: [[1]] -[[0]] přístupový token je: [[1]] diff --git a/emails/translations/sms-messages_da.txt b/emails/translations/sms-messages_da.txt deleted file mode 100644 index f6f39e34a6..0000000000 --- a/emails/translations/sms-messages_da.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] verifikations kode er: [[1]] -[[0]] adgangs token er: [[1]] diff --git a/emails/translations/sms-messages_de.txt b/emails/translations/sms-messages_de.txt deleted file mode 100644 index 8ff877f88b..0000000000 --- a/emails/translations/sms-messages_de.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] Bestätigungscode lautet: [[1]] -[[0]] Zugriffstoken ist: [[1]] diff --git a/emails/translations/sms-messages_es.txt b/emails/translations/sms-messages_es.txt deleted file mode 100644 index 68a40081c8..0000000000 --- a/emails/translations/sms-messages_es.txt +++ /dev/null @@ -1,2 +0,0 @@ -El código de verificación de [[0]] es: [[1]] -El token de acceso de [[0]] es: [[1]] diff --git a/emails/translations/sms-messages_fi.txt b/emails/translations/sms-messages_fi.txt deleted file mode 100644 index 86bc6f51ae..0000000000 --- a/emails/translations/sms-messages_fi.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] vahvistuskoodi on: [[1]] -[[0]] pääsy koodi on: [[1]] diff --git a/emails/translations/sms-messages_fr.txt b/emails/translations/sms-messages_fr.txt deleted file mode 100644 index fda9fe69dc..0000000000 --- a/emails/translations/sms-messages_fr.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] le code de vérification est: [[1]] -[[0]] le jeton d'accès est: [[1]] diff --git a/emails/translations/sms-messages_hi.txt b/emails/translations/sms-messages_hi.txt deleted file mode 100644 index 81e0954b6e..0000000000 --- a/emails/translations/sms-messages_hi.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] सत्यापन कोड है: [[1]] -[[0]] अभिगमन टोकन है: [[१]] diff --git a/emails/translations/sms-messages_hu.txt b/emails/translations/sms-messages_hu.txt deleted file mode 100644 index 07b75b61ef..0000000000 --- a/emails/translations/sms-messages_hu.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] ellenőrző kód: [[1]] -[[0]] hozzáférés token: [[1]] diff --git a/emails/translations/sms-messages_it.txt b/emails/translations/sms-messages_it.txt deleted file mode 100644 index 6ede48550e..0000000000 --- a/emails/translations/sms-messages_it.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] il codice di verifica è: [[1]] -[0]] token di accesso è: [[1]] diff --git a/emails/translations/sms-messages_ja.txt b/emails/translations/sms-messages_ja.txt deleted file mode 100644 index a1a9414914..0000000000 --- a/emails/translations/sms-messages_ja.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]]確認コード:[[1]] -[[0]]アクセストークンは:[[1]] diff --git a/emails/translations/sms-messages_ko.txt b/emails/translations/sms-messages_ko.txt deleted file mode 100644 index 997bc7b1ad..0000000000 --- a/emails/translations/sms-messages_ko.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] 인증된 코드는 다음과 같습니다 : [[1]] -[[0]] 접근 토큰은 다음과 같습니다 : [[1]] diff --git a/emails/translations/sms-messages_nl.txt b/emails/translations/sms-messages_nl.txt deleted file mode 100644 index 249be2aadf..0000000000 --- a/emails/translations/sms-messages_nl.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] verificatie code is: [[1]] -[[0]] Toegangs token is: [[1]] diff --git a/emails/translations/sms-messages_pl.txt b/emails/translations/sms-messages_pl.txt deleted file mode 100644 index efb43ce004..0000000000 --- a/emails/translations/sms-messages_pl.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] kod weryfikacyjny to: [[1]] -[[0]] token dostępu to: [[1]] diff --git a/emails/translations/sms-messages_pt-br.txt b/emails/translations/sms-messages_pt-br.txt deleted file mode 100644 index 6e1f0f695e..0000000000 --- a/emails/translations/sms-messages_pt-br.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] código de verificação é: [[1]] -[[0]] token de acesso é: [[1]] diff --git a/emails/translations/sms-messages_pt.txt b/emails/translations/sms-messages_pt.txt deleted file mode 100644 index 6e1f0f695e..0000000000 --- a/emails/translations/sms-messages_pt.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] código de verificação é: [[1]] -[[0]] token de acesso é: [[1]] diff --git a/emails/translations/sms-messages_ru.txt b/emails/translations/sms-messages_ru.txt deleted file mode 100644 index 7c2cbc3a2b..0000000000 --- a/emails/translations/sms-messages_ru.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] код подтверждения: [[1]] -[[0]] токен доступа: [[1]] diff --git a/emails/translations/sms-messages_sv.txt b/emails/translations/sms-messages_sv.txt deleted file mode 100644 index 970fa91915..0000000000 --- a/emails/translations/sms-messages_sv.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] verifieringskod är: [[1]] -[[0]] åtkomsttoken är: [[1]] diff --git a/emails/translations/sms-messages_tr.txt b/emails/translations/sms-messages_tr.txt deleted file mode 100644 index 921f4c35de..0000000000 --- a/emails/translations/sms-messages_tr.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]] doğrulama kodu: [[1]] -[[0]] erişim anahtarı: [[1]] diff --git a/emails/translations/sms-messages_zh-chs.txt b/emails/translations/sms-messages_zh-chs.txt deleted file mode 100644 index 9d2e320e24..0000000000 --- a/emails/translations/sms-messages_zh-chs.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]]验证码是:[[1]] -[[0]]访问令牌是:[[1]] diff --git a/emails/translations/sms-messages_zh-cht.txt b/emails/translations/sms-messages_zh-cht.txt deleted file mode 100644 index bb274698ef..0000000000 --- a/emails/translations/sms-messages_zh-cht.txt +++ /dev/null @@ -1,2 +0,0 @@ -[[0]]驗證碼是:[[1]] -[[0]]訪問保安編碼是:[[1]] diff --git a/firebase.js b/firebase.js index 124acfa07b..36d7c94824 100644 --- a/firebase.js +++ b/firebase.js @@ -166,7 +166,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { const WebSocket = require('ws'); const https = require('https'); const querystring = require('querystring'); - const relayUrl = require('url').parse(url); + const relayUrl = new URL(url); parent.debug('email', 'CreateFirebaseRelay-Setup'); // Setup logging @@ -294,7 +294,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { const httpOptions = { hostname: relayUrl.hostname, port: relayUrl.port ? relayUrl.port : 443, - path: relayUrl.path + (key ? ('?key=' + key) : ''), + path: relayUrl.pathname + (key ? ('?key=' + key) : ''), method: 'POST', //rejectUnauthorized: false, // DEBUG headers: { diff --git a/macosinstaller.js b/macosinstaller.js new file mode 100644 index 0000000000..015a4d776b --- /dev/null +++ b/macosinstaller.js @@ -0,0 +1,391 @@ +/* + * @description Cross-platform macOS flat package builder for MeshAgent installers. + * Creates a XAR-based distribution package instead of the legacy bundle .mpkg + * format that macOS Sequoia/Tahoe rejects. + */ + +'use strict'; + +const crypto = require('crypto'); +const fs = require('fs'); +const fsp = fs.promises; +const os = require('os'); +const path = require('path'); +const zlib = require('zlib'); +const childProcess = require('child_process'); +const { promisify } = require('util'); + +const deflate = promisify(zlib.deflate); +const execFile = promisify(childProcess.execFile); + +const LAUNCH_DAEMON_PLIST = ` + + + + Label + ###SERVICENAME### + ProgramArguments + + /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME### + + WorkingDirectory + /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/ + RunAtLoad + + KeepAlive + + ThrottleInterval + 1 + + +`; + +const LAUNCH_AGENT_PLIST = ` + + + + Label + ###SERVICENAME###-launchagent + ProgramArguments + + /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME### + -kvmagent + + LimitLoadToSessionType + + LoginWindow + Aqua + + WorkingDirectory + /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/ + RunAtLoad + + KeepAlive + + + +`; + +const POSTINSTALL = `#!/bin/bash +set -e + +SERVICENAME="###SERVICENAME###" +COMPANYNAME="###COMPANYNAME###" +EXECUTABLENAME="###EXECUTABLENAME###" +INSTALLDIR="/usr/local/mesh_services/\${COMPANYNAME}/\${SERVICENAME}" + +chown -R root:wheel "/usr/local/mesh_services/\${COMPANYNAME}" || true +chown root:wheel "\${INSTALLDIR}/\${EXECUTABLENAME}" "\${INSTALLDIR}/\${EXECUTABLENAME}.msh" +chown root:wheel "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" + +chmod 755 "\${INSTALLDIR}" "\${INSTALLDIR}/\${EXECUTABLENAME}" +chmod 644 "\${INSTALLDIR}/\${EXECUTABLENAME}.msh" "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" + +/bin/launchctl bootout system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || true +/bin/launchctl bootstrap system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || /bin/launchctl load "/Library/LaunchDaemons/\${SERVICENAME}.plist" + +CONSOLE_USER=$(stat -f '%Su' /dev/console 2>/dev/null || true) +CONSOLE_UID=$(id -u "\${CONSOLE_USER}" 2>/dev/null || true) +if [ -n "\${CONSOLE_UID}" ] && [ "\${CONSOLE_UID}" != "0" ]; then + /bin/launchctl bootout "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" >/dev/null 2>&1 || true + /bin/launchctl bootstrap "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" >/dev/null 2>&1 || true +fi +`; + +const UNINSTALL = `#!/bin/bash + +echo "Stopping ###SERVICENAME###..." +sudo /bin/launchctl bootout system "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null || sudo /bin/launchctl unload "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null +sudo pkill -9 "###SERVICENAME###" &> /dev/null || true +CONSOLE_USER=$(stat -f '%Su' /dev/console 2>/dev/null || true) +CONSOLE_UID=$(id -u "\${CONSOLE_USER}" 2>/dev/null || true) +if [ -n "\${CONSOLE_UID}" ] && [ "\${CONSOLE_UID}" != "0" ]; then + sudo /bin/launchctl bootout "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/###SERVICENAME###-launchagent.plist" &> /dev/null || true +fi +sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null +sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null +sudo rm "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null +sudo rm "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null + +echo "Resetting TCC permissions for ###SERVICENAME###..." +BUNDLE_ID=$(mdls -name kMDItemCFBundleIdentifier -raw "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###" 2>/dev/null || true) +if [ -n "\${BUNDLE_ID}" ] && [ "\${BUNDLE_ID}" != "(null)" ]; then + sudo tccutil reset All "\${BUNDLE_ID}" &> /dev/null || true +fi +sudo tccutil reset All "###SERVICENAME###" &> /dev/null || true + +sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###" &> /dev/null +sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.msh" &> /dev/null +sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.db" &> /dev/null +sudo rm "/usr/local/mesh_services/meshagentDiagnostic/meshagentDiagnostic" &> /dev/null +sudo rm "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null +sudo rm "/Library/LaunchAgents/###SERVICENAME###-launchagent.plist" &> /dev/null +echo "###SERVICENAME### was uninstalled." +`; + +function replaceTokens(str, tokens) { + return str.split('###SERVICENAME###').join(tokens.serviceName) + .split('###COMPANYNAME###').join(tokens.companyName) + .split('###EXECUTABLENAME###').join(tokens.executableName); +} + +function xmlEscape(str) { + return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); +} + +function pkgIdentifierSegment(str) { + return String(str).toLowerCase().replace(/[^a-z0-9.-]/g, '-').replace(/^-+|-+$/g, '') || 'meshagent'; +} + +async function chmodIfExists(file, mode) { + try { await fsp.chmod(file, mode); } catch (ex) { } +} + +async function walk(dir) { + const entries = await fsp.readdir(dir, { withFileTypes: true }); + let files = 0, bytes = 0; + for (const entry of entries) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) { + const r = await walk(p); + files += r.files; + bytes += r.bytes; + } else if (entry.isFile()) { + const s = await fsp.stat(p); + files++; + bytes += s.size; + } + } + return { files, bytes }; +} + +function pad4(buffer) { + const pad = (4 - (buffer.length % 4)) % 4; + return (pad === 0) ? buffer : Buffer.concat([buffer, Buffer.alloc(pad)]); +} + +function octal(value, width) { + const max = Math.pow(8, width) - 1; + const n = Math.max(0, Math.min(Number(value) || 0, max)); + return Math.floor(n).toString(8).padStart(width, '0').slice(-width); +} + +async function collectPayloadEntries(root, relativePath) { + const fullPath = path.join(root, relativePath); + const stat = await fsp.stat(fullPath); + const entries = []; + if (relativePath !== '') { + entries.push({ name: relativePath.split(path.sep).join('/'), stat: stat, data: stat.isFile() ? await fsp.readFile(fullPath) : null }); + } + if (stat.isDirectory()) { + const names = (await fsp.readdir(fullPath)).sort(); + for (const name of names) { entries.push.apply(entries, await collectPayloadEntries(root, path.join(relativePath, name))); } + } + return entries; +} + +function cpioOdcRecord(name, mode, data, ino, mtime) { + data = data || Buffer.alloc(0); + const nameBuffer = Buffer.from(name + '\0', 'utf8'); + const header = [ + '070707', + octal(0, 6), // dev + octal(ino, 6), + octal(mode, 6), + octal(0, 6), // uid + octal(0, 6), // gid + octal(1, 6), // nlink + octal(0, 6), // rdev + octal(mtime || Math.floor(Date.now() / 1000), 11), + octal(nameBuffer.length, 6), + octal(data.length, 11) + ].join(''); + return Buffer.concat([Buffer.from(header, 'ascii'), nameBuffer, data]); +} + +async function createPayload(payloadRoot, targetFile) { + const entries = await collectPayloadEntries(payloadRoot, ''); + const records = []; + let ino = 1; + for (const entry of entries) { + records.push(cpioOdcRecord(entry.name, entry.stat.mode, entry.data, ino++, Math.floor(entry.stat.mtimeMs / 1000))); + } + records.push(cpioOdcRecord('TRAILER!!!', 0, Buffer.alloc(0), ino)); + await fsp.writeFile(targetFile, zlib.gzipSync(Buffer.concat(records))); +} + +async function createBom(payloadRoot, targetFile) { + try { + await execFile('mkbom', [payloadRoot, targetFile], { timeout: 30000 }); + } catch (ex) { + // Linux/Windows hosts can still build the package archive without a + // third-party BOM dependency. macOS hosts use the native mkbom tool + // above so local validation keeps the richer bill of materials. + await fsp.writeFile(targetFile, Buffer.alloc(0)); + } +} + +async function collectXarEntry(filePath, name, id) { + const stat = await fsp.stat(filePath); + const entry = { + id: id, + name: name, + type: stat.isDirectory() ? 'directory' : 'file', + mode: stat.mode, + uid: stat.uid, + gid: stat.gid, + atime: stat.atime, + mtime: stat.mtime, + ctime: stat.ctime + }; + if (stat.isFile()) { + entry.data = await fsp.readFile(filePath); + } else if (stat.isDirectory()) { + const names = (await fsp.readdir(filePath)).sort(); + entry.children = []; + for (const childName of names) { + entry.children.push(await collectXarEntry(path.join(filePath, childName), childName, ++collectXarEntry.nextId)); + } + } + return entry; +} + +function xarDate(d) { + return d.toISOString(); +} + +function xarFileXml(entry, depth, heapParts) { + const indent = ' '.repeat(depth); + let xml = indent + '\n' + + indent + ' ' + xmlEscape(entry.name) + '\n' + + indent + ' ' + entry.type + '\n' + + indent + ' ' + entry.mode.toString(8) + '\n' + + indent + ' ' + entry.uid + '\n' + + indent + ' ' + entry.gid + '\n' + + indent + ' ' + xarDate(entry.atime) + '\n' + + indent + ' ' + xarDate(entry.mtime) + '\n' + + indent + ' ' + xarDate(entry.ctime) + '\n'; + if (entry.type == 'file') { + const offset = 20 + heapParts.reduce(function (total, part) { return total + part.length; }, 0); + const sum = crypto.createHash('sha1').update(entry.data).digest('hex'); + heapParts.push(entry.data); + xml += indent + ' \n' + + indent + ' ' + sum + '\n' + + indent + ' ' + sum + '\n' + + indent + ' ' + offset + '\n' + + indent + ' \n' + + indent + ' ' + entry.data.length + '\n' + + indent + ' ' + entry.data.length + '\n' + + indent + ' \n'; + } else { + for (const child of entry.children) { xml += xarFileXml(child, depth + 1, heapParts); } + } + return xml + indent + '\n'; +} + +async function createXarPackage(paths) { + collectXarEntry.nextId = 0; + const entries = []; + for (const p of paths) { entries.push(await collectXarEntry(p, path.basename(p), ++collectXarEntry.nextId)); } + + const heapParts = []; + let toc = '\n\n \n' + + ' \n 20\n 0\n \n' + + ' ' + (new Date()).toISOString() + '\n'; + for (const entry of entries) { toc += xarFileXml(entry, 2, heapParts); } + toc += ' \n'; + + const tocBuffer = Buffer.from(toc, 'utf8'); + const compressedToc = await deflate(tocBuffer); + const tocChecksum = crypto.createHash('sha1').update(compressedToc).digest(); + const header = Buffer.alloc(28); + header.writeUInt32BE(0x78617221, 0); // xar! + header.writeUInt16BE(28, 4); + header.writeUInt16BE(1, 6); + header.writeBigUInt64BE(BigInt(compressedToc.length), 8); + header.writeBigUInt64BE(BigInt(tocBuffer.length), 16); + header.writeUInt32BE(1, 24); // sha1 + return Buffer.concat([header, compressedToc, tocChecksum].concat(heapParts)); +} + +async function createMacOSInstaller(opts) { + const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'meshcentral-macos-pkg-')); + try { + const payloadRoot = path.join(tmpRoot, 'payload'); + const scriptsRoot = path.join(tmpRoot, 'scripts'); + const basePkg = path.join(tmpRoot, 'internal.pkg'); + const resourcesDir = path.join(tmpRoot, 'Resources'); + const installDir = path.join(payloadRoot, 'usr', 'local', 'mesh_services', opts.companyName, opts.serviceName); + const launchDaemons = path.join(payloadRoot, 'Library', 'LaunchDaemons'); + const launchAgents = path.join(payloadRoot, 'Library', 'LaunchAgents'); + const tokens = { serviceName: opts.serviceName, companyName: opts.companyName, executableName: opts.executableName }; + + await fsp.mkdir(installDir, { recursive: true }); + await fsp.mkdir(launchDaemons, { recursive: true }); + await fsp.mkdir(launchAgents, { recursive: true }); + await fsp.mkdir(basePkg, { recursive: true }); + await fsp.mkdir(scriptsRoot, { recursive: true }); + await fsp.mkdir(resourcesDir, { recursive: true }); + + await fsp.copyFile(opts.agentPath, path.join(installDir, opts.executableName)); + await fsp.writeFile(path.join(installDir, opts.executableName + '.msh'), opts.meshSettings); + await fsp.writeFile(path.join(launchDaemons, opts.serviceName + '.plist'), replaceTokens(LAUNCH_DAEMON_PLIST, tokens)); + await fsp.writeFile(path.join(launchAgents, opts.serviceName + '-launchagent.plist'), replaceTokens(LAUNCH_AGENT_PLIST, tokens)); + await fsp.writeFile(path.join(scriptsRoot, 'postinstall'), replaceTokens(POSTINSTALL, tokens)); + + if (opts.backgroundPath) { + await fsp.copyFile(opts.backgroundPath, path.join(resourcesDir, 'background')); + } else { + const backgroundPath = path.join(__dirname, 'agents', 'macosinstallerbackground.png'); + await fsp.copyFile(backgroundPath, path.join(resourcesDir, 'background')); + } + + await chmodIfExists(path.join(installDir, opts.executableName), 0o755); + await chmodIfExists(path.join(scriptsRoot, 'postinstall'), 0o755); + await chmodIfExists(path.join(installDir, opts.executableName + '.msh'), 0o644); + await chmodIfExists(path.join(launchDaemons, opts.serviceName + '.plist'), 0o644); + await chmodIfExists(path.join(launchAgents, opts.serviceName + '-launchagent.plist'), 0o644); + + const payloadStats = await walk(payloadRoot); + const installKBytes = Math.ceil(payloadStats.bytes / 1000); + await createPayload(payloadRoot, path.join(basePkg, 'Payload')); + await createPayload(scriptsRoot, path.join(basePkg, 'Scripts')); + await createBom(payloadRoot, path.join(basePkg, 'Bom')); + + const packageInfo = '\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '\n'; + await fsp.writeFile(path.join(basePkg, 'PackageInfo'), packageInfo); + + const welcome = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + opts.meshName + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n'; + const distribution = '\n' + + '\n' + + ' ' + xmlEscape(opts.displayName) + '\n' + + ' \n' + + ' \n' + + ' ').join(']]]]>') + ']]>\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' #internal.pkg\n' + + ' \n' + + '\n'; + await fsp.writeFile(path.join(tmpRoot, 'Distribution'), distribution); + + const pkgBuffer = await createXarPackage([basePkg, resourcesDir, path.join(tmpRoot, 'Distribution')]); + return { + pkg: pkgBuffer, + uninstall: replaceTokens(UNINSTALL, tokens) + }; + } finally { + await fsp.rm(tmpRoot, { recursive: true, force: true }); + } +} + +module.exports = { createMacOSInstaller }; diff --git a/mcrec.js b/mcrec.js index 6761227807..d23afcdbfe 100644 --- a/mcrec.js +++ b/mcrec.js @@ -119,6 +119,7 @@ function processBlock(state, block, err) { switch (command) { case 3: // Tile + if (state.swidth == null) { break; } var x = block.data.readUInt16BE(4); var y = block.data.readUInt16BE(6); var dimensions = require('image-size').imageSize(block.data.slice(8)); diff --git a/meshagent.js b/meshagent.js index 07a5f7e1e5..412126d561 100644 --- a/meshagent.js +++ b/meshagent.js @@ -98,7 +98,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { db.Remove('si' + obj.dbNodeKey); // Remove system information db.Remove('al' + obj.dbNodeKey); // Remove error log last time if (db.RemoveSMBIOS) { db.RemoveSMBIOS(obj.dbNodeKey); } // Remove SMBios data - db.RemoveAllNodeEvents(obj.dbNodeKey); // Remove all events for this node + db.RemoveAllNodeEvents(domain.id, obj.dbNodeKey); // Remove all events for this node db.removeAllPowerEventsForNode(obj.dbNodeKey); // Remove all power events for this node // Event node deletion @@ -208,7 +208,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Clear the core obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core parent.agentStats.clearingCoreCount++; - parent.parent.debug('agent', "Clearing core"); + parent.parent.debug('agent', "Clearing core for agent " + obj.nodeid); } else { // Setup task limiter options, this system limits how many tasks can run at the same time to spread the server load. var taskLimiterOptions = { hash: meshcorehash, core: parent.parent.defaultMeshCores[corename], name: corename }; @@ -226,7 +226,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { delete obj.agentCoreUpdatePending; obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + argument.hash + argument.core.toString('binary'), function () { parent.parent.taskLimiter.completed(taskid); }); // MeshCommand_CoreModule, start core update parent.agentStats.updatingCoreCount++; - parent.parent.debug('agent', "Updating core " + argument.name); + parent.parent.debug('agent', "Updating core " + argument.name + " for agent " + obj.nodeid); } else { // This agent is probably disconnected, nothing to do. parent.parent.taskLimiter.completed(taskid); @@ -1224,6 +1224,12 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey); break; } + case 'software': + { + // Todo - save software into database for offline access but send to web clients for now + parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey); + break; + } case 'coreinfo': { // Sent by the agent to update agent information @@ -1237,7 +1243,8 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Agent update. The recovery core was loaded in the agent, send a command to update the agent obj.agentCoreUpdateTaskId = taskid; - const url = '*' + require('url').parse(obj.agentExeInfo.url).path; + const getme = new URL(obj.agentExeInfo.url); + const url = '*' + getme.pathname + getme.search; var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex }; parent.parent.debug('agentupdate', "Sending agent update url: " + cmd.url); @@ -1454,11 +1461,43 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { command.data.type = 'sysinfo'; command.data.domain = domain.id; command.data.time = Date.now(); - db.Set(command.data); // Update system information in the database. - // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed - var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 }; - parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event); + // Store the document and notify viewers that the sysinfo hash changed. + var saveSysInfo = function () { + db.Set(command.data); + // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed + var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 }; + parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event); + }; + + var volumes = command.data.hardware?.windows?.volumes; + if (volumes) { + // BitLocker recovery keys are kept in hardware.windows.bitlocker, keyed by protector identifier + // (decoupled from the drive letter, which can change). A key is retained for 'bitlockerKeyRetentionDays' + // days after it was last read; 0 (default) disables carry-forward, keeping only keys read in this scan. + var ttl = (parent.parent.config.settings?.bitlockerkeyretentiondays > 0) ? (parent.parent.config.settings.bitlockerkeyretentiondays * 86400000) : 0; + var updateBLKeys = function (prevKeys) { + var keys = {}; + // Carry forward keys last read within the retention window. + if ((ttl > 0) && prevKeys) { + for (const id in prevKeys) { if ((command.data.time - prevKeys[id].t) <= ttl) { keys[id] = prevKeys[id]; } } + } + // Record keys actually read this scan (refreshes the timestamp). + for (const v of Object.values(volumes)) { + if (v.identifier && v.recoveryPassword) { keys[v.identifier] = { rp: v.recoveryPassword, t: command.data.time }; } + } + command.data.hardware.windows.bitlocker = keys; + saveSysInfo(); + }; + if (ttl > 0) { + // Need the previous doc to carry keys forward. + db.Get(command.data._id, function (err, nodes) { updateBLKeys((nodes && nodes.length > 0) ? nodes[0].hardware?.windows?.bitlocker : null); }); + } else { + updateBLKeys(null); // no carry-forward, no previous-doc read needed + } + } else { + saveSysInfo(); // non-Windows + } } break; } @@ -1476,6 +1515,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { if (command.type == 'kvm') { obj.sessions.kvm = command.value; } else if (command.type == 'terminal') { obj.sessions.terminal = command.value; } else if (command.type == 'files') { obj.sessions.files = command.value; } + else if (command.type == 'registry') { obj.sessions.registry = command.value; } else if (command.type == 'help') { obj.sessions.help = command.value; } else if (command.type == 'tcp') { obj.sessions.tcp = command.value; } else if (command.type == 'udp') { obj.sessions.udp = command.value; } @@ -1584,7 +1624,8 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Agent is requesting an agent update obj.agentCoreUpdateTaskId = taskid; - const url = '*' + require('url').parse(obj.agentExeInfo.url).path; + const getme = new URL(obj.agentExeInfo.url); + const url = '*' + getme.pathname + getme.search; var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid }; parent.parent.debug('agentupdate', "Sending user requested agent update url: " + cmd.url); @@ -1629,12 +1670,13 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { if ((typeof command.url != 'string') || (typeof command.approved != 'boolean') || (command.url.startsWith('2fa://') == false)) return; // parse the URL + // Node 22's WHATWG URL parser rejects custom schemes like 2fa://, so swap to https:// for parsing only. var url = null; - try { url = require('url').parse(command.url); } catch (ex) { } + try { url = new URL(command.url.replace(/^2fa:\/\//, 'https://')); } catch (ex) { } if (url == null) return; // Decode the cookie - var urlSplit = url.query.split('&c='); + var urlSplit = url.search.slice(1).split('&c='); if (urlSplit.length != 2) return; const authCookie = parent.parent.decodeCookie(urlSplit[1], null, 1); if ((authCookie == null) || (typeof authCookie.c != 'string') || (('code=' + authCookie.c) != urlSplit[0])) return; @@ -1925,6 +1967,10 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { if (!device.wsc) { device.wsc = {}; } if (JSON.stringify(device.wsc) != JSON.stringify(command.wsc)) { /*changes.push('Windows Security Center status');*/ device.wsc = command.wsc; change = 1; log = 1; } } + if (command.lsc != null) { // Linux Security Center + if (!device.lsc) { device.lsc = {}; } + if (JSON.stringify(device.lsc) != JSON.stringify(command.lsc)) { /*changes.push('Linux Security Center status');*/ device.lsc = command.lsc; change = 1; log = 1; } + } if (command.defender != null) { // Defender For Windows Server if (!device.defender) { device.defender = {}; } if (JSON.stringify(device.defender) != JSON.stringify(command.defender)) { /*changes.push('Defender status');*/ device.defender = command.defender; change = 1; log = 1; } diff --git a/meshbot.js b/meshbot.js index 5ff0de9b33..1c946ada0c 100644 --- a/meshbot.js +++ b/meshbot.js @@ -61,7 +61,7 @@ function serverConnect() { // Setup the HTTP proxy if needed if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); - options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); + options.agent = new HttpsProxyAgent(new URL(args.proxy)); } // Authentication setup diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index dc5da1aeb6..55b71b084c 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -569,6 +569,11 @@ "default": false, "description": "Enables agent-side, websocket per-message deflate compression. wscompression must also be true for this to work." }, + "bitlockerKeyRetentionDays": { + "type": "integer", + "default": 0, + "description": "How many days to retain a BitLocker recovery key after the agent last read it, so a key stays available while a volume is locked or detached. Especially useful for Bitlocker To Go devices. 0 (default) disables carry-forward: only keys read in the latest scan are kept." + }, "noAgentUpdate": { "type": "integer", "default": 0, @@ -1453,6 +1458,11 @@ } } }, + "softwareInventory": { + "type": "boolean", + "default": false, + "description": "When set to true, the Software Inventory feature is enabled." + }, "autoRemoveInactiveDevices": { "type": "integer", "default": 0, @@ -1951,6 +1961,15 @@ "default": false, "description": "When enabled, device groups will be collapsed by default in the devices tab." }, + "terminalUserVariable": { + "type": "string", + "enum": [ + "realname", + "identifier", + "username" + ], + "description": "Adds MESHCENTRAL_USER environment variable to linux terminal sessions using either 'realname', 'identifier', or 'username'." + }, "userSessionsSort": { "type": "string", "default": "SessionId", @@ -2221,6 +2240,11 @@ "default": null, "description": "The filename of a image file in .png format located in meshcentral-data to display in the MeshCentral Agent installation dialog, image should be square and from 64x64 to 200x200." }, + "macosinstallerimage": { + "type": "string", + "default": null, + "description": "The filename of a background image file in .png format located in meshcentral-data to display in the macOS installer." + }, "fileName": { "type": "string", "default": "meshagent", @@ -3996,15 +4020,13 @@ "description": "REQUIRED IF USING GROUPS: Customer ID from Google Workspace Admin Console (https://admin.google.com/ac/accountsettings/profile)" }, "authorities": { - "type": [ - "array" - ], - "description": "Default value is groups. Roles can be used in azure to assign app permissons", - "enum": [ - "groups", - "roles" - ] + "type": "array", + "description": "Default value is groups. Roles can be used in azure to assign app permissions", + "items": { + "type": "string", + "enum": ["groups", "roles"] } + } } }, "groups": { diff --git a/meshcentral.js b/meshcentral.js index 0723d5d3c8..436b3a33ac 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -15,6 +15,7 @@ "use strict"; const common = require('./common.js'); +const { zipExtract } = require('./backup.js'); // If app metrics is available if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } } @@ -163,7 +164,7 @@ function CreateMeshCentralServer(config, args) { 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', - 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy' + 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy', 'migratevolumeinfo' ]; for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } } const ENVVAR_PREFIX = "meshcentral_" @@ -189,25 +190,31 @@ function CreateMeshCentralServer(config, args) { console.log('Details at: https://www.meshcentral.com\r\n'); if ((obj.platform == 'win32') || (obj.platform == 'linux')) { console.log('Run as a background service'); - console.log(' --install/uninstall Install MeshCentral as a background service.'); - console.log(' --start/stop/restart Control MeshCentral background service.'); + console.log(' --install/uninstall\t\t\t\t\tInstall MeshCentral as a background service.'); + console.log(' --start/stop/restart\t\t\t\t\tControl MeshCentral background service.'); console.log(''); console.log('Run standalone, console application'); } - console.log(' --user [username] Always login as [username] if account exists.'); - console.log(' --port [number] Web server port number.'); - console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.'); - console.log(' --exactports Server must run with correct ports or exit.'); - console.log(' --noagentupdate Server will not update mesh agent native binaries.'); - console.log(' --nedbtodb Transfer all NeDB records into current database.'); - console.log(' --listuserids Show a list of a user identifiers in the database.'); - console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.'); - console.log(' country and organization can optionally be set.'); - console.log(''); - console.log('Server recovery commands, use only when MeshCentral is offline.'); - console.log(' --createaccount [userid] Create a new user account.'); - console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.'); - console.log(' --adminaccount [userid] Promote account to site administrator.'); + console.log(' --user [username]\t\t\t\t\tAlways login as [username] if account exists.'); + console.log(' --port [number]\t\t\t\t\tWeb server port number.'); + console.log(' --redirport [number]\t\t\t\t\tCreates an additional HTTP server to redirect users to the HTTPS server.'); + console.log(' --exactports\t\t\t\t\t\tServer must run with correct ports or exit.'); + console.log(' --noagentupdate\t\t\t\t\tServer will not update mesh agent native binaries.'); + console.log(' --nedbtodb\t\t\t\t\t\tTransfer all NeDB records into current database.'); + console.log(' --listuserids\t\t\t\t\tShow a list of a user identifiers in the database.'); + console.log(' --cert [name], (country), (org)\t\t\tCreate a web server certificate with [name] server name.'); + console.log('\t\t\t\t\t\t\tcountry and organization can optionally be set.'); + console.log('\r\nRun stateless, config files (config.json and certificates) are stored encrypted with [password] in the database.'); + console.log(' --dblistconfigfile (password)\t\t\tShow the files in the db and optionally check the files against the password.'); + console.log(' --dbpushconfigfiles (path) --configkey [password]\tThis will import the current configfiles or from (path) into the database.'); + console.log(' --dbshowconfigfile [file] --configkey [password]\tShow contents of config [file].'); + console.log(' --dbpullconfigfiles [path] --configkey [password]\tPull configfiles out of the database into [path].'); + console.log(' --dbdeleteconfigfiles\t\t\t\tDelete config files from the database.'); + console.log(' --loadconfigfromdb [password]\t\t\tRun meshcentral with configfiles found in the database.') + console.log('\r\nServer recovery commands, use only when MeshCentral is offline.'); + console.log(' --createaccount [userid]\t\t\t\tCreate a new user account.'); + console.log(' --resetaccount [userid]\t\t\t\tUnlock an account, disable 2FA and set a new account password.'); + console.log(' --adminaccount [userid]\t\t\t\tPromote account to site administrator.'); return; } @@ -990,6 +997,7 @@ function CreateMeshCentralServer(config, args) { if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; } if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; } if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; } + if (obj.args.migratevolumeinfo) { require('./migrate-volume-info.js').migrateVolumeInfo(obj.db, function (err, r) { if (err != null) { console.log('Volume info migration error: ' + err); } else { console.log('Volume info migration complete. Scanned ' + r.scanned + ' sysinfo document(s), migrated ' + r.migrated + ', moved ' + r.keysMoved + ' key(s).'); } process.exit(); }); return; } if (obj.args.createaccount) { // Create a new user account if ((typeof obj.args.createaccount != 'string') || ((obj.args.pass == null) && (obj.args.hashpass == null)) || (obj.args.pass == '') || (obj.args.hashpass == '') || (obj.args.createaccount.indexOf(' ') >= 0)) { console.log("Usage: --createaccount [userid] --pass [password] --domain (domain) --email (email) --name (name)."); process.exit(); return; } var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : ''; @@ -1024,7 +1032,7 @@ function CreateMeshCentralServer(config, args) { if (err != null) { console.log("Database error: " + err); process.exit(); return; } if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; } const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account. - delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; user.otpduo; // Disable 2FA + delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; delete user.otpduo; // Disable 2FA delete user.msghandle; // Disable users 2fa messaging too var config = getConfig(false); if (config.domains[user.domain].auth || config.domains[user.domain].authstrategies) { @@ -1098,7 +1106,7 @@ function CreateMeshCentralServer(config, args) { db.Remove('lc' + node._id); // Remove last connect time db.Remove('si' + node._id); // Remove system information if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data - db.RemoveAllNodeEvents(node._id); // Remove all events for this node + db.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token db.Get('ra' + node._id, function (err, nodes) { @@ -1175,7 +1183,7 @@ function CreateMeshCentralServer(config, args) { if (err == null) { if (docs.length == 0) { console.log("File not found."); } else { const data = obj.db.decryptData(obj.args.configkey, docs[0].data); - if (data == null) { console.log("Invalid config key."); } else { console.log(data); } + if (data == null) { console.log("Invalid config key."); } else { console.log(data.toString()); } } } else { console.log("Unable to read from database."); } process.exit(); @@ -1819,23 +1827,25 @@ function CreateMeshCentralServer(config, args) { } // Load CloudFlare trusted proxies list if needed - if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) { + const trustedproxyIsCloudflareString = (obj.args.trustedproxy != null) && (typeof obj.args.trustedproxy == 'string') && (obj.args.trustedproxy.toLowerCase() == 'cloudflare'); + const trustedproxyIsCloudflareArray = Array.isArray(obj.args.trustedproxy) && obj.args.trustedproxy.some(function (x) { return (typeof x == 'string') && (x.toLowerCase() == 'cloudflare'); }); + if (trustedproxyIsCloudflareString || trustedproxyIsCloudflareArray) { obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature. - delete obj.args.trustedproxy; - delete obj.config.settings.trustedproxy; + // Preserve any non-'cloudflare' entries already in the array + const existingProxies = trustedproxyIsCloudflareArray ? obj.args.trustedproxy.filter(function (x) { return !(typeof x == 'string' && x.toLowerCase() == 'cloudflare'); }) : []; obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) { if (data != null) { - if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; } + const newProxies = existingProxies.slice(); const ipranges = data.split('\n'); - for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } } + for (var i in ipranges) { if (ipranges[i] != '') { newProxies.push(ipranges[i]); } } obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) { if (data != null) { var ipranges = data.split('\n'); - for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } } - obj.config.settings.trustedproxy = obj.args.trustedproxy; + for (var i in ipranges) { if (ipranges[i] != '') { newProxies.push(ipranges[i]); } } } else { addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16); } + obj.args.trustedproxy = newProxies; obj.StartEx4(); // Keep going }); } else { @@ -2148,6 +2158,15 @@ function CreateMeshCentralServer(config, args) { if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions else { if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; }; + if (Object.hasOwn(obj.config.settings.autobackup, "zippassword")) { + if ((obj.config.settings.autobackup.zippassword).length == 0) { + delete obj.config.settings.autobackup.zippassword; + obj.addServerWarning('Empty zip password in config.json', true); + } else { + // convert to string regardless of type + obj.config.settings.autobackup.zippassword = String(obj.config.settings.autobackup.zippassword); + } + } if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; }; if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; }; if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }} @@ -2355,49 +2374,25 @@ function CreateMeshCentralServer(config, args) { obj.Stop = function (restoreFile) { // If the database is not setup, exit now. if (!obj.db) return; - // Dispatch an event saying the server is now stopping obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" }); - + const restorePassword = obj.config.settings.autobackup.zippasswordrequest; + delete obj.config.settings.autobackup.zippasswordrequest; // Set all nodes to power state of unknown (0) obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down. if (restoreFile) { obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile)); - console.log("Updating settings folder..."); - - const yauzl = require('yauzl'); - yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) { - if (err) throw err; - zipfile.readEntry(); - zipfile.on('entry', function (entry) { - if (/\/$/.test(entry.fileName)) { - // Directory file names end with '/'. - // Note that entires for directories themselves are optional. - // An entry's fileName implicitly requires its parent directories to exist. - zipfile.readEntry(); - } else { - // File entry - zipfile.openReadStream(entry, function (err, readStream) { - if (err) throw err; - readStream.on('end', function () { zipfile.readEntry(); }); - var directory = obj.path.dirname(entry.fileName); - if (directory != '.') { - directory = obj.getConfigFilePath(directory) - if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); } - } - //console.log('Extracting:', obj.getConfigFilePath(entry.fileName)); - readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName))); - }); - } + console.log("Updating settings folder..."); // do not alter. This specific log message, with the process.exit(123) further on, triggers a process restart. See obj.launchChildServer>childProcess.stdout.on function + zipExtract(restoreFile, obj.datapath, 'meshcentral-data/', restorePassword) + .then((res) => { + res['res'] ? console.log(res['mes']) : console.error(res['mes']); + process.exit(123); // this triggers the childserver process restart }); - zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); }); - }); } else { obj.debug('main', "Server stopped"); process.exit(0); } }); - // Update the server state obj.updateServerState('state', "stopped"); }; @@ -2418,8 +2413,8 @@ function CreateMeshCentralServer(config, args) { delete obj.eventsDispatch[id]; } else { const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch. - for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } } - obj.eventsDispatch[i] = newList; + for (var k in obj.eventsDispatch[id]) { if (obj.eventsDispatch[id][k] != target) { newList.push(obj.eventsDispatch[id][k]); } } + obj.eventsDispatch[id] = newList; } } } @@ -3311,11 +3306,22 @@ function CreateMeshCentralServer(config, args) { if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; } else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; } - // Setup the pending operations counter - var pendingOperations = 1; - + // Collect architectures that need signing + var archIds = []; for (var archid in obj.meshAgentsArchitectureNumbers) { if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue; + archIds.push(archid); + } + + if (archIds.length === 0) { func(); return; } + + var currentArchIndex = 0; + + function signNextAgent() { + if (currentArchIndex >= archIds.length) { func(); return; } + + var archid = archIds[currentArchIndex]; + currentArchIndex++; var agentpath; if (domain.id == '') { @@ -3326,7 +3332,7 @@ function CreateMeshCentralServer(config, args) { } else { // When processing an extra domain, only load agents that are specific to that domain agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname); - if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip. + if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { signNextAgent(); return; } // If the agent is not present in "meshcentral-data/agents" skip. } // Open the original agent with authenticode @@ -3459,9 +3465,10 @@ function CreateMeshCentralServer(config, args) { addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]); } obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure - if (--pendingOperations === 0) { agentSignedFunc.func(); } + // Wait 2 seconds between each codesign to avoid rate limiting from Sectigo's timestamp server + // https://www.sectigo.com/resource-library/time-stamping-server + setTimeout(signNextAgent, 2000); } - pendingOperations++; xagentSignedFunc.func = func; xagentSignedFunc.objx = objx; xagentSignedFunc.archid = archid; @@ -3530,13 +3537,15 @@ function CreateMeshCentralServer(config, args) { } else { // Signed agent is already ok, use it. originalAgent.close(); + signNextAgent(); } - + } else { + signNextAgent(); } } - if (--pendingOperations === 0) { func(); } + signNextAgent(); } obj.callExternalSignJob = function (signingArguments) { @@ -4332,23 +4341,13 @@ function mainStart() { if (mstsc == false) { config.domains[i].mstsc = false; } if (config.domains[i].ssh == true) { ssh = true; } if ((typeof config.domains[i].authstrategies == 'object')) { - if (passport.indexOf('passport') == -1) { passport.push('passport@0.7.0','connect-flash@0.1.1'); } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors - if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter@1.0.4'); } - if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20@2.0.0'); } - if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2@0.1.12'); } - if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2@0.1.0'); passport.push('jwt-simple@0.5.6'); } - if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) { - if ((nodeVersion >= 17) - || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13)) - || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15)) - || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) { - passport.push('openid-client@5.7.1'); - } else { - addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25); - delete config.domains[i].authstrategies.oidc; - } - } - if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); } + passport.push('passport@0.7.0','connect-flash@0.1.1'); // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors + if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string')) { passport.push('passport-twitter@1.0.4'); } + if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string')) { passport.push('passport-google-oauth20@2.0.0'); } + if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string')) { passport.push('passport-github2@0.1.12'); } + if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string')) { passport.push('passport-azure-oauth2@0.1.0'); passport.push('jwt-simple@0.5.6'); } + if (typeof config.domains[i].authstrategies.oidc == 'object') { passport.push('openid-client@5.7.1'); } + if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml@3.2.4'); } } if (config.domains[i].sessionrecording != null) { sessionRecording = true; } if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; } @@ -4358,11 +4357,11 @@ function mainStart() { // Build the list of required modules // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile - var modules = ['archiver@7.0.1', 'body-parser@1.20.4', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.22.1', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@seald-io/nedb@4.1.2', 'node-forge@1.3.2', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.18.3', 'yauzl@2.10.0']; + var modules = ['archiver@7.0.1', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.22.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.3.0', '@seald-io/nedb@4.1.2', 'node-forge@1.4.0', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.21.0', 'yauzl@2.10.0', '@zip.js/zip.js@2.8.26']; // Base modules if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); } if (ssh == true) { modules.push('ssh2@1.17.0'); } - if (passport != null) { modules.push(...passport); } + if (passport != null) { passport = passport.filter((value, index, self) => self.indexOf(value) === index); modules.push(...passport); } if (captcha == true) { modules.push('svg-captcha@1.4.0'); } if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file. @@ -4405,7 +4404,7 @@ function mainStart() { if (config.settings.no2factorauth !== true) { // Setup YubiKey OTP if configured if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues) - if (allsspi == false) { modules.push('otplib@12.0.1'); } // Google Authenticator support (v10 supports older NodeJS versions). + if (allsspi == false) { modules.push('otplib@13.4.1'); } // Google Authenticator support (v10 supports older NodeJS versions). } // Desktop multiplexor support diff --git a/meshctrl.js b/meshctrl.js index dc1d902097..4e6115cf07 100644 --- a/meshctrl.js +++ b/meshctrl.js @@ -578,7 +578,7 @@ if (args['_'].length == 0) { console.log(" 4096 = Desktop Limited Input 8192 = Limit Events "); console.log(" 16384 = Chat / Notify 32768 = Uninstall Agent "); console.log(" 65536 = No Remote Desktop 131072 = Remote Commands "); - console.log(" 262144 = Reset / Power off "); + console.log(" 262144 = Reset / Power off 4194304 = No Registry "); break; } case 'removefromusergroup': { @@ -714,6 +714,7 @@ if (args['_'].length == 0) { console.log(" --limiteddesktop - Limit remote desktop keys."); console.log(" --noterminal - Hide the terminal tab from this user."); console.log(" --nofiles - Hide the files tab from this user."); + console.log(" --noregistry - Hide the registry tab from this user."); console.log(" --noamt - Hide the Intel AMT tab from this user."); console.log(" --limitedevents - User can only see his own events."); console.log(" --chatnotify - Allow chat and notification options."); @@ -760,6 +761,7 @@ if (args['_'].length == 0) { console.log(" --limiteddesktop - Limit remote desktop keys."); console.log(" --noterminal - Hide the terminal tab from this user."); console.log(" --nofiles - Hide the files tab from this user."); + console.log(" --noregistry - Hide the registry tab from this user."); console.log(" --noamt - Hide the Intel AMT tab from this user."); console.log(" --limitedevents - User can only see his own events."); console.log(" --chatnotify - Allow chat and notification options."); @@ -865,6 +867,8 @@ if (args['_'].length == 0) { case 'editdevice': { console.log("Change information about a device, Example usages:\r\n"); console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'")); + console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --addtag 'newtag'")); + console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --removetag 'oldtag'")); console.log("\r\nRequired arguments:\r\n"); if (process.platform == 'win32') { console.log(" --id [deviceid] - The device identifier."); @@ -873,13 +877,17 @@ if (args['_'].length == 0) { } console.log("\r\nOptional arguments:\r\n"); if (process.platform == 'win32') { - console.log(" --name [name] - Change device name."); - console.log(" --desc [description] - Change device description."); - console.log(" --tags [tag1,tags2] - Change device tags."); + console.log(" --name [name] - Change device name."); + console.log(" --desc [description] - Change device description."); + console.log(" --tags [tag1,tag2] - Set device tags (replaces all existing tags)."); + console.log(" --addtag [tag1,tag2] - Add tags to existing tags."); + console.log(" --removetag [tag1,tag2] - Remove tags from existing tags."); } else { - console.log(" --name '[name]' - Change device name."); - console.log(" --desc '[description]' - Change device description."); - console.log(" --tags '[tag1,tags2]' - Change device tags."); + console.log(" --name '[name]' - Change device name."); + console.log(" --desc '[description]' - Change device description."); + console.log(" --tags '[tag1,tag2]' - Set device tags (replaces all existing tags)."); + console.log(" --addtag '[tag1,tag2]' - Add tags to existing tags."); + console.log(" --removetag '[tag1,tag2]' - Remove tags from existing tags."); } console.log(" --icon [number] - Change the device icon (1 to 8)."); console.log(" --consent [flags] - Sum of the following numbers:"); @@ -1326,7 +1334,7 @@ function serverConnect() { // Setup the HTTP proxy if needed if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); - options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); + options.agent = new HttpsProxyAgent(new URL(args.proxy)); } // Password authentication @@ -1648,12 +1656,13 @@ function serverConnect() { if (args.desktopviewonly) { meshrights |= 256; } if (args.noterminal) { meshrights |= 512; } if (args.nofiles) { meshrights |= 1024; } + if (args.noregistry) { meshrights |= 4194304; } if (args.noamt) { meshrights |= 2048; } if (args.limiteddesktop) { meshrights |= 4096; } if (args.limitedevents) { meshrights |= 8192; } if (args.chatnotify) { meshrights |= 16384; } if (args.uninstall) { meshrights |= 32768; } - var op = { action: 'addmeshuser', usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' }; + var op = { action: 'addmeshuser', userids: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' }; if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; } ws.send(JSON.stringify(op)); break; @@ -1675,6 +1684,7 @@ function serverConnect() { if (args.desktopviewonly) { meshrights |= 256; } if (args.noterminal) { meshrights |= 512; } if (args.nofiles) { meshrights |= 1024; } + if (args.noregistry) { meshrights |= 4194304; } if (args.noamt) { meshrights |= 2048; } if (args.limiteddesktop) { meshrights |= 4096; } if (args.limitedevents) { meshrights |= 8192; } @@ -1728,14 +1738,20 @@ function serverConnect() { break; } case 'editdevice': { - var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' }; - if (typeof args.name == 'string') { op.name = args.name; } - if (typeof args.name == 'number') { op.name = '' + args.name; } - if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } } - if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } } - if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } } - if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } } - ws.send(JSON.stringify(op)); + if (args.addtag || args.removetag) { + // we need to fetch the node data first to then modify the tags + var nodeid = args.id; + ws.send(JSON.stringify({ action: 'nodes', id: args.id, responseid: 'meshctrl' })); + } else { + var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' }; + if (typeof args.name == 'string') { op.name = args.name; } + if (typeof args.name == 'number') { op.name = '' + args.name; } + if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } } + if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } } + if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } } + if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } } + ws.send(JSON.stringify(op)); + } break; } case 'runcommand': { @@ -2132,7 +2148,8 @@ function serverConnect() { if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) { var protocol = 1; // Terminal if ((settings.cmd == 'upload') || (settings.cmd == 'download')) { protocol = 5; } // Files - if ((args.id.split('/') != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; } + if (args.powershell) { protocol = 6; } // PowerShell + if ((args.id.split('/').length != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; } var id = getRandomHex(6); ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' })); connectTunnel(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + protocol + '&nodeid=' + encodeURIComponent(args.id) + '&id=' + id + '&auth=' + data.cookie)); @@ -2472,6 +2489,43 @@ function serverConnect() { } } } + if ((settings.cmd == 'editdevice') && (data.responseid == 'meshctrl')) { + // Find the node to get its current tags + var targetNode = null; + for (var i in data.nodes) { + for (var j in data.nodes[i]) { + if (data.nodes[i][j]._id == args.id) { targetNode = data.nodes[i][j]; break; } + } + if (targetNode != null) { break; } + } + if (targetNode == null) { + console.log('Node not found.'); + process.exit(); + return; + } + // Start with current tags or empty array + var tags = (Array.isArray(targetNode.tags)) ? targetNode.tags.slice() : []; + // Add tags: --addtag tag1,tag2 + if (args.addtag) { + var addtags = (typeof args.addtag == 'string') ? args.addtag.split(',') : ['' + args.addtag]; + for (var i in addtags) { var t = addtags[i].trim(); if (t && (tags.indexOf(t) < 0)) { tags.push(t); } } + } + // Remove tags: --removetag tag1,tag2 + if (args.removetag) { + var removetags = (typeof args.removetag == 'string') ? args.removetag.split(',') : ['' + args.removetag]; + var removetrimmed = removetags.map(function(r) { return r.trim(); }); + tags = tags.filter(function(t) { return removetrimmed.indexOf(t) < 0; }); + } + // Build and send the changedevice op + var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' }; + if (typeof args.name == 'string') { op.name = args.name; } + if (typeof args.name == 'number') { op.name = '' + args.name; } + if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } } + if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } } + if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } } + op.tags = tags; + ws.send(JSON.stringify(op)); + } break; } case 'meshes': { // LISTDEVICEGROUPS @@ -2705,7 +2759,7 @@ function getDevicesThatMatchFilter(nodes, x) { } else { // Device name search try { - var rs = x.split(/\s+/).join('|'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception. + var rs = x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception. for (var d in nodes) { //if (showRealNames) { //if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase())) { r.push(nodes[d]); } @@ -2726,7 +2780,7 @@ function connectTunnel(url) { var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer } // Setup the HTTP proxy if needed - if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); } + if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(new URL(args.proxy)); } // Connect the WebSocket console.log('Connecting...'); @@ -2756,7 +2810,7 @@ function connectTunnel(url) { if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); } if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'options', cols: termSize[0], rows: termSize[1] })); } settings.tunnelwsstate = 1; - settings.tunnelws.send('1'); // Terminal + settings.tunnelws.send((args.powershell ? 6 : 1)); // Powershell or Terminal process.stdin.setEncoding('utf8'); process.stdin.setRawMode(true); process.stdout.setEncoding('utf8'); @@ -2926,7 +2980,7 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) { if (node.desc != null) { output["Description"] = node.desc; outputCount++; } if (node.icon != null) { output["Icon"] = node.icon; outputCount++; } if (node.tags) { output["Tags"] = node.tags; outputCount++; } - if (node.av) { + if (node.av && node.av.length > 0) { var av = []; for (var i in node.av) { if (typeof node.av[i]['product'] == 'string') { @@ -2943,16 +2997,24 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) { if (typeof node.wsc == 'object') { output["WindowsSecurityCenter"] = node.wsc; outputCount++; } + if (typeof node.lsc == 'object') { + output["LinuxSecurityCenter"] = node.lsc; outputCount++; + } if (outputCount > 0) { info["General"] = output; } // Operating System var hardware = null; if ((sysinfo != null) && (sysinfo.hardware != null)) { hardware = sysinfo.hardware; } - if ((hardware && hardware.windows && hardware.windows.osinfo) || node.osdesc) { + if ((hardware && hardware.windows && hardware.windows.osinfo) || (hardware && hardware.linux) || node.osdesc) { var output = {}, outputCount = 0; if (node.rname) { output["Name"] = node.rname; outputCount++; } if (node.osdesc) { output["Version"] = node.osdesc; outputCount++; } if (hardware && hardware.windows && hardware.windows.osinfo) { var m = hardware.windows.osinfo; if (m.OSArchitecture) { output["Architecture"] = m.OSArchitecture; outputCount++; } } + if (hardware && hardware.linux) { + if (hardware.linux.arch) { output["Architecture"] = hardware.linux.arch; outputCount++; } + if (hardware.linux.kernel_release) { output["Kernel Release"] = hardware.linux.kernel_release; outputCount++; } + if (hardware.linux.kernel_build) { output["Kernel Build"] = hardware.linux.kernel_build; outputCount++; } + } if (outputCount > 0) { info["Operating System"] = output; } } @@ -3078,6 +3140,8 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) { // BIOS if (ident.bios_vendor) { output["Vendor"] = ident.bios_vendor; outputCount++; } if (ident.bios_version) { output["Version"] = ident.bios_version; outputCount++; } + if (ident.bios_serial) { output["Serial"] = ident.bios_serial; outputCount++; } + if (ident.bios_mode) { output["Mode"] = ident.bios_mode; outputCount++; } if (outputCount > 0) { info["BIOS"] = output; } output = {}, outputCount = 0; @@ -3090,6 +3154,28 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) { if (ident.cpu_name) { output["CPU"] = ident.cpu_name; } if (ident.gpu_name) { for (var i in ident.gpu_name) { output["GPU" + (parseInt(i) + 1)] = ident.gpu_name[i]; } } if (outputCount > 0) { info["Motherboard"] = output; } + output = {}, outputCount = 0; + + // System + if (ident.chassis_manufacturer) { output["Manufacturer"] = ident.chassis_manufacturer; outputCount++; } + if (ident.product_name) { output["Product Name"] = ident.product_name; outputCount++; } + if (ident.chassis_serial) { output["Serial"] = ident.chassis_serial; outputCount++; } + if (ident.chassis_assettag) { output["Asset Tag"] = ident.chassis_assettag; outputCount++; } + if (outputCount > 0) { info["System"] = output; } + output = {}, outputCount = 0; + } + + // TPM + if (hardware.tpm) { + var output = {}, outputCount = 0, tpm = hardware.tpm; + if (tpm.SpecVersion) { output["SpecVersion"] = parseFloat(tpm.SpecVersion).toFixed(1); outputCount++; } + if (tpm.ManufacturerId) { output["Identifier"] = tpm.ManufacturerId; outputCount++; } + if (tpm.ManufacturerVersion) { output["Version"] = tpm.ManufacturerVersion; outputCount++; } + if (tpm.IsActivated != null) { output["Activated"] = (tpm.IsActivated ? "Yes" : "No"); outputCount++; } + if (tpm.IsEnabled != null) { output["Enabled"] = (tpm.IsEnabled ? "Yes" : "No"); outputCount++; } + if (tpm.IsOwned != null) { output["Owned"] = (tpm.IsOwned ? "Yes" : "No"); outputCount++; } + if (outputCount > 0) { info["TPM"] = output; } + output = {}, outputCount = 0; } // Memory @@ -3099,9 +3185,10 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) { hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; }); for (var i in hardware.windows.memory) { var m = hardware.windows.memory[i], moutput = {}, moutputCount = 0; - if (m.Capacity) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; } + if (m.Capacity && m.Speed) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; } + else if (m.Capacity) { moutput["Capacity"] = (m.Capacity / 1024 / 1024) + " Mb"; moutputCount++; } if (m.PartNumber) { moutput["Part Number"] = ((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + m.PartNumber; moutputCount++; } - if (moutputCount > 0) { minfo[m.BankLabel] = moutput; info["Memory"] = minfo; } + if (moutputCount > 0) { minfo[m.BankLabel ? m.BankLabel : (m.DeviceLocator ? m.DeviceLocator : 'Unknown')] = moutput; info["Memory"] = minfo; } } } } @@ -3122,6 +3209,12 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) { } } } + + // Windows volumes + if ((hardware?.windows?.volumes)) { info["Volumes"] = hardware.windows.volumes; } + + // Bitlocker cache + if ((hardware?.windows?.bitlocker)) { info["Bitlocker cache"] = hardware.windows.bitlocker; } } // Display everything diff --git a/meshdesktopmultiplex.js b/meshdesktopmultiplex.js index b4550faff5..3dd93cbf35 100644 --- a/meshdesktopmultiplex.js +++ b/meshdesktopmultiplex.js @@ -1340,6 +1340,7 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) { if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; } const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?p=2&id=' + obj.id + '&rauth=' + rcookie + '&nodeid=' + node._id, soptions: {}, usage: 2, rights: cookie.r, guestuserid: user._id, guestname: cookie.gn, consent: cookie.cf, remoteaddr: cleanRemoteAddr(obj.req.clientIp) }; + if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; } if (typeof domain.consentmessages == 'object') { if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; } @@ -1350,7 +1351,7 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) { if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; } if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; } if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; } - if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; } + if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; } if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; } if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; } if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; } diff --git a/meshipkvm.js b/meshipkvm.js index 2623eb9040..ead44363d5 100644 --- a/meshipkvm.js +++ b/meshipkvm.js @@ -34,6 +34,7 @@ function CreateIPKVMManager(parent) { const MESHRIGHT_RESETOFF = 0x00040000; // 262144 const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288 const MESHRIGHT_DEVICEDETAILS = 0x00100000; // ?1048576? + const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304 const MESHRIGHT_ADMIN = 0xFFFFFFFF; // Subscribe for mesh creation events @@ -262,10 +263,10 @@ function CreateIPKVMManager(parent) { // Parse an incoming HTTP request URL function parseIpKvmUrl(domain, url) { - const q = require('url').parse(url, true); - const i = q.path.indexOf('/ipkvm.ashx/'); + const q = new URL(url, 'http://localhost'); + const i = q.pathname.indexOf('/ipkvm.ashx/'); if (i == -1) return null; - const urlargs = q.path.substring(i + 12).split('/'); + const urlargs = q.pathname.substring(i + 12).split('/'); if (urlargs[0].length != 64) return null; const nodeid = 'node/' + domain.id + '/' + urlargs[0]; const nid = urlargs[0]; @@ -1079,4 +1080,4 @@ function CreateMiniRouter(parent, nodeid, targetHost, targetPort) { return obj; } -module.exports.CreateIPKVMManager = CreateIPKVMManager; \ No newline at end of file +module.exports.CreateIPKVMManager = CreateIPKVMManager; diff --git a/meshmail.js b/meshmail.js index 2c88051e82..330c61c7d0 100644 --- a/meshmail.js +++ b/meshmail.js @@ -31,6 +31,10 @@ module.exports.CreateMeshMail = function (parent, domain) { const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) const constants = (obj.parent.crypto.constants ? obj.parent.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead. + // Safe JSON stringify resistant to circular references. + // Node TLS errors (e.g. SMTP cert mismatch) include the certificate chain, where each X509 cert has an 'issuerCertificate' field pointing back at its issuer, closing the cycle. Plain JSON.stringify throws "Converting circular structure to JSON" and crashes the server. This helper drops cert-related fields and tracks seen objects via a WeakSet. + function safeStringify(o) { try { const seen = new WeakSet(); return JSON.stringify(o, function (k, v) { if (typeof v === 'object' && v !== null) { if (seen.has(v)) return '[Circular]'; seen.add(v); } if (k === 'issuerCertificate' || k === 'cert' || k === 'raw') return '[Cert]'; return v; }); } catch (ex) { return '[stringify-error: ' + ex.message + ']'; } } + function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&').replace(/>/g, '>').replace(//g, '>').replace(/').replace(/\n/g, '').replace(/\t/g, '  '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; } @@ -488,15 +492,15 @@ module.exports.CreateMeshMail = function (parent, domain) { sendNextMail(); }, function (error) { obj.sendingMail = false; - parent.debug('email', 'SendGrid sending error: ' + JSON.stringify(error)); + parent.debug('email', 'SendGrid sending error: ' + safeStringify(error)); obj.retry++; // Wait and try again if (obj.retry < 3) { setTimeout(sendNextMail, 10000); } else { // Failed, send the next mail - parent.debug('email', 'SendGrid server failed (Skipping): ' + JSON.stringify(err)); - console.log('SendGrid server failed (Skipping): ' + JSON.stringify(err)); + parent.debug('email', 'SendGrid server failed (Skipping): ' + safeStringify(err)); + console.log('SendGrid server failed (Skipping): ' + safeStringify(err)); obj.pendingMails.shift(); obj.retry = 0; sendNextMail(); @@ -516,7 +520,7 @@ module.exports.CreateMeshMail = function (parent, domain) { } else { // SMTP send obj.smtpServer.sendMail(mailToSend, function (err, info) { - parent.debug('email', 'SMTP response: ' + JSON.stringify(err) + ', ' + JSON.stringify(info)); + parent.debug('email', 'SMTP response: ' + safeStringify(err) + ', ' + JSON.stringify(info)); obj.sendingMail = false; if (err == null) { // Send the next mail @@ -525,15 +529,15 @@ module.exports.CreateMeshMail = function (parent, domain) { sendNextMail(); } else { obj.retry++; - parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + JSON.stringify(err)); - console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + JSON.stringify(err)); + parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + safeStringify(err)); + console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + safeStringify(err)); // Wait and try again if (obj.retry < 3) { setTimeout(sendNextMail, 10000); } else { // Failed, send the next mail - parent.debug('email', 'SMTP server failed (Skipping): ' + JSON.stringify(err)); - console.log('SMTP server failed (Skipping): ' + JSON.stringify(err)); + parent.debug('email', 'SMTP server failed (Skipping): ' + safeStringify(err)); + console.log('SMTP server failed (Skipping): ' + safeStringify(err)); obj.pendingMails.shift(); obj.retry = 0; sendNextMail(); @@ -558,8 +562,8 @@ module.exports.CreateMeshMail = function (parent, domain) { // Remove all non-object types from error to avoid a JSON stringify error. var err2 = {}; for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } } - parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2)); - console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2)); + parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + safeStringify(err2)); + console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + safeStringify(err2)); } }); }; diff --git a/meshmessaging.js b/meshmessaging.js index 9a722792a0..073937844b 100644 --- a/meshmessaging.js +++ b/meshmessaging.js @@ -334,7 +334,7 @@ module.exports.CreateServer = function (parent) { console.log('Sending CallMeBot message to: ' + to.substring(10) + ': ' + msg); var toData = to.substring(10).split('|'); if ((toData[0] == 'signal') && (toData.length == 3)) { - var url = 'https://api.callmebot.com/signal/send.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg); + var url = 'https://signal.callmebot.com/signal/send.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg); require('https').get(url, function (r) { if (func != null) { func(r.statusCode == 200); } }); } else if ((toData[0] == 'whatsapp') && (toData.length == 3)) { var url = 'https://api.callmebot.com/whatsapp.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg); @@ -365,7 +365,11 @@ module.exports.CreateServer = function (parent) { }); if (func != null) { func(true); } }else if ((to.startsWith('slack:')) && (obj.slackClient != null)) { //slack - const req = require('https').request(new URL(to.substring(6)), { method: 'POST' }, function (res) { if (func != null) { func(true); } }); + // Slack incoming webhooks are always on hooks.slack.com, so only allow that host. + var slackUrl; + try { slackUrl = new URL(to.substring(6)); } catch (ex) { if (func != null) { func(false); } return; } + if (slackUrl.hostname != 'hooks.slack.com') { if (func != null) { func(false); } return; } + const req = require('https').request(slackUrl, { method: 'POST' }, function (res) { if (func != null) { func(true); } }); req.on('error', function (err) { if (func != null) { func(false); } }); req.write(JSON.stringify({"text": msg })); req.end(); @@ -378,18 +382,18 @@ module.exports.CreateServer = function (parent) { // Convert a CallMeBot URL into a handle obj.callmebotUrlToHandle = function (xurl) { var url = null; - try { url = require('url').parse(xurl); } catch (ex) { return; } - if ((url == null) || (url.host != 'api.callmebot.com') || (url.query == null)) return; - var urlArgs = {}, urlArgs2 = url.query.split('&'); + try { url = new URL(xurl); } catch (ex) { return; } + if ((url == null) || !url.host.endsWith('.callmebot.com') || !url.search) return; + var urlArgs = {}, urlArgs2 = url.search.slice(1).split('&'); for (var i in urlArgs2) { var j = urlArgs2[i].indexOf('='); if (j > 0) { urlArgs[urlArgs2[i].substring(0, j)] = urlArgs2[i].substring(j + 1); } } if ((urlArgs['phone'] != null) && (urlArgs['phone'].indexOf('|') >= 0)) return; if ((urlArgs['apikey'] != null) && (urlArgs['apikey'].indexOf('|') >= 0)) return; if ((urlArgs['user'] != null) && (urlArgs['user'].indexOf('|') >= 0)) return; // Signal Messenger, Whatapp, Facebook and Telegram - if (url.path.startsWith('/signal') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:signal|' + urlArgs['phone'] + '|' + urlArgs['apikey']; } - if (url.path.startsWith('/whatsapp') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:whatsapp|' + urlArgs['phone'] + '|' + urlArgs['apikey']; } - if (url.path.startsWith('/facebook') && (urlArgs['apikey'] != null)) { return 'callmebot:facebook|' + urlArgs['apikey']; } - if (url.path.startsWith('/text') && (urlArgs['user'] != null)) { return 'callmebot:telegram|' + urlArgs['user']; } + if (url.pathname.startsWith('/signal') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:signal|' + urlArgs['phone'] + '|' + urlArgs['apikey']; } + if (url.pathname.startsWith('/whatsapp') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:whatsapp|' + urlArgs['phone'] + '|' + urlArgs['apikey']; } + if (url.pathname.startsWith('/facebook') && (urlArgs['apikey'] != null)) { return 'callmebot:facebook|' + urlArgs['apikey']; } + if (url.pathname.startsWith('/text') && (urlArgs['user'] != null)) { return 'callmebot:telegram|' + urlArgs['user']; } return null; } diff --git a/meshrelay.js b/meshrelay.js index 116add1f2b..1d6440d40d 100644 --- a/meshrelay.js +++ b/meshrelay.js @@ -36,11 +36,13 @@ const MESHRIGHT_RESETOFF = 0x00040000; // 262144 const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288 const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576 const MESHRIGHT_RELAY = 0x00200000; // 2097152 +const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304 const MESHRIGHT_ADMIN = 0xFFFFFFFF; // Protocol: // 1 = Terminal // 2 = Desktop +// 4 = Registry // 5 = Files // 6 = Admin PowerShell // 8 = User Shell @@ -68,6 +70,15 @@ function checkDeviceSharePublicIdentifier(parent, domain, nodeid, pid, extraKey, }); } +function isProtocolAllowedByRights(rights, protocol) { + if ((rights == null) || (rights == MESHRIGHT_ADMIN)) { return true; } + if ((protocol == 1) && ((rights & MESHRIGHT_NOTERMINAL) != 0)) { return false; } + if ((protocol == 2) && ((rights & MESHRIGHT_NODESKTOP) != 0)) { return false; } + if ((protocol == 4) && ((rights & MESHRIGHT_NOREGISTRY) != 0)) { return false; } + if ((protocol == 5) && ((rights & MESHRIGHT_NOFILES) != 0)) { return false; } + return true; +} + module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie) { if ((cookie != null) && (typeof cookie.nid == 'string') && (typeof cookie.pid == 'string')) { checkDeviceSharePublicIdentifier(parent, domain, cookie.nid, cookie.pid, cookie.k, function (result) { @@ -885,7 +896,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { const node = docs[0]; // Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights) - if ((obj.nouser !== true) && ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0)) { console.log('ERR: Access denied (1)'); try { obj.close(); } catch (ex) { } return; } + const rights = parent.GetNodeRights(obj.user, node.meshid, node._id); + if ((obj.nouser !== true) && ((rights & 0x00200008) == 0)) { console.log('ERR: Access denied (1)'); try { obj.close(); } catch (ex) { } return; } + if ((obj.nouser !== true) && (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false)) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; } // Set nodeid and meshid obj.nodeid = node._id; @@ -898,6 +911,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (obj.id == null) { obj.id = parent.crypto.randomBytes(9).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } // If there is no connection id, generate one. const command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/' + xdomain + 'meshrelay.ashx?' + (obj.req.query.p != null ? ('p=' + obj.req.query.p + '&') : '') + 'id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr, soptions: {} }; if (user) { command.userid = user._id; } + if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; } if (typeof domain.consentmessages == 'object') { if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; } @@ -908,7 +922,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; } if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; } if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; } - if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; } + if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; } if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; } if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; } if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; } @@ -933,7 +947,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { const node = docs[0]; // Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights) - if ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + const rights = parent.GetNodeRights(obj.user, node.meshid, node._id); + if ((rights & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + if (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; } // Set nodeid and meshid obj.nodeid = node._id; @@ -954,7 +970,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; } if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; } if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; } - if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; } + if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; } if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; } if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; } if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; } @@ -980,7 +996,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; } if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; } if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; } - if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; } + if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; } if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; } if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; } if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; } @@ -1006,7 +1022,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { const node = docs[0]; // Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights) - if ((obj.nouser !== true) && ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + const rights = parent.GetNodeRights(obj.user, node.meshid, node._id); + if ((obj.nouser !== true) && ((rights & 0x00200008) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + if ((obj.nouser !== true) && (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false)) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; } // Set nodeid and meshid obj.nodeid = node._id; @@ -1040,7 +1058,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; } if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; } if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; } - if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; } + if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; } if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; } if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; } if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; } @@ -1069,7 +1087,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { const node = docs[0]; // Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights) - if ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + const rights = parent.GetNodeRights(obj.user, node.meshid, node._id); + if ((rights & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + if (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; } // Set nodeid and meshid obj.nodeid = node._id; @@ -1316,7 +1336,9 @@ function CreateLocalRelayEx(parent, ws, req, domain, user, cookie) { obj.meshid = node.meshid; // Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights) - if ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + const rights = parent.GetNodeRights(obj.user, node.meshid, node._id); + if ((rights & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; } + if (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; } // Setup TCP client obj.client = new net.Socket(); diff --git a/meshsms.js b/meshsms.js index 441e7c4d3c..6619a32eaf 100644 --- a/meshsms.js +++ b/meshsms.js @@ -147,16 +147,16 @@ module.exports.CreateMeshSMS = function (parent) { } else { var sms = parent.config.sms.url.split('{{phone}}').join(encodeURIComponent(to)).split('{{message}}').join(encodeURIComponent(msg)); parent.debug('email', 'SMS URL: ' + sms); - sms = require('url').parse(sms); + sms = new URL(sms); if (sms.protocol == 'https:') { // HTTPS GET request - const options = { hostname: sms.hostname, port: sms.port ? sms.port : 443, path: sms.path, method: 'GET', rejectUnauthorized: false }; + const options = { hostname: sms.hostname, port: sms.port ? sms.port : 443, path: sms.pathname + sms.search, method: 'GET', rejectUnauthorized: false }; const request = require('https').request(options, function (res) { parent.debug('email', 'SMS result: ' + res.statusCode); if (func != null) { func(res.statusCode == 200, (res.statusCode == 200) ? null : res.statusCode, null); } res.on('data', function (d) { }); }); request.on('error', function (err) { parent.debug('email', 'SMS error: ' + err); if (func != null) { func(false, err, null); } }); request.end(); } else { // HTTP GET request - const options = { hostname: sms.hostname, port: sms.port ? sms.port : 80, path: sms.path, method: 'GET' }; + const options = { hostname: sms.hostname, port: sms.port ? sms.port : 80, path: sms.pathname + sms.search, method: 'GET' }; const request = require('http').request(options, function (res) { parent.debug('email', 'SMS result: ' + res.statusCode); if (func != null) { func(res.statusCode == 200, (res.statusCode == 200) ? null : res.statusCode, null); } res.on('data', function (d) { }); }); request.on('error', function (err) { parent.debug('email', 'SMS error: ' + err); if (func != null) { func(false, err, null); } }); request.end(); diff --git a/meshuser.js b/meshuser.js index 34013e1a8b..53afd2460c 100644 --- a/meshuser.js +++ b/meshuser.js @@ -13,6 +13,12 @@ /*jshint esversion: 6 */ "use strict"; +// volume & bitlocker statuses +const encMethod = { 0: '', 1: "AES-128 with diffuser", 2: "AES-256 with diffuser", 3: 'AES-128', 4: 'AES-256', 5: "Hardware encryption", 6: 'XTS-AES-128', 7: 'XTS-AES-256' }; +const driveType = { 0: "Unknown", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" }; +const conversionStatus = { "-1": "Unknown", 0: "Fully Decrypted", 1: "Fully Encrypted", 2: "Encryption In Progress", 3: "Decryption In Progress", 4: "Encryption Paused", 5: "Decryption Paused" }; +const protectionStatus = { 0: "Off", 1: "On", 2: "Locked"}; + // Construct a MeshAgent object, called upon connection module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, user) { const fs = require('fs'); @@ -53,6 +59,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288 const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576 const MESHRIGHT_RELAY = 0x00200000; // 2097152 + const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304 const MESHRIGHT_ADMIN = 0xFFFFFFFF; // Site rights @@ -646,6 +653,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } serverinfo.preConfiguredScripts = r; } + serverinfo.softwareinventory = domain?.softwareinventory === true; if (domain.maxdeviceview != null) { serverinfo.maxdeviceview = domain.maxdeviceview; } // Maximum number of devices a user can view at any given time // Send server information @@ -729,6 +737,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (command.meshid == null) { err = 'Invalid group id'; } } + // Check if command.id is set with a domain id, if not, add the domain id to it. + if (typeof command.id == 'string' && command.id !== '') { + if (common.validateString(command.id, 1, 1024) == false) { err = 'Invalid device identifier'; } + else if (command.id.indexOf('/') == -1) { command.id = 'node/' + domain.id + '/' + command.id; } + } + if (err == null) { try { if (command.meshid == null) { @@ -971,6 +985,26 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'software': { + if (domain.softwareinventory !== true) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } } + break; + } + parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) { + var mesh = parent.meshes[node.meshid]; + if ((node != null) && (mesh != null) && ((rights & MESHRIGHT_DEVICEDETAILS) != 0)) { + var agent = parent.wsagents[command.nodeid]; + if (agent != null) { + routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions); + } else { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Agent offline' })); } catch (ex) { } } + } + } else { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } } + } + }); + break; + } case 'msg': { // Check the nodeid @@ -994,16 +1028,17 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (command.type == 'tunnel') { if ((typeof command.value != 'string') || (typeof command.nodeid != 'string')) break; var url = null; - try { url = require('url').parse(command.value, true); } catch (ex) { } + try { url = new URL(command.value, 'http://localhost'); } catch (ex) { } if (url == null) break; // Bad URL - if (url.query && url.query.nodeid && (url.query.nodeid != command.nodeid)) break; // Bad NodeID in URL query string + if (url.searchParams.get('nodeid') && (url.searchParams.get('nodeid') != command.nodeid)) break; // Bad NodeID in URL query string // Check rights - if (url.query.p == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; } - else if ((url.query.p == '4') || (url.query.p == '5')) { requiredNonRights = MESHRIGHT_NOFILES; } + if (url.searchParams.get('p') == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; } + else if (url.searchParams.get('p') == '4') { requiredNonRights = MESHRIGHT_NOREGISTRY; } + else if (url.searchParams.get('p') == '5') { requiredNonRights = MESHRIGHT_NOFILES; } // If we are using the desktop multiplexor, remove the VIEWONLY limitation. The multiplexor will take care of enforcing that limitation when needed. - if (((parent.parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (url.query.p == '2')) { routingOptions = { removeViewOnlyLimitation: true }; } + if (((parent.parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (url.searchParams.get('p') == '2')) { routingOptions = { removeViewOnlyLimitation: true }; } // Add server TLS cert hash var tlsCertHash = null; @@ -1024,7 +1059,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; } if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; } if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; } - if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; } + if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; } if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; } if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; } if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; } @@ -1037,6 +1072,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (typeof domain.notificationmessages.terminal == 'string') { command.soptions.notifyMsgTerminal = domain.notificationmessages.terminal; } if (typeof domain.notificationmessages.files == 'string') { command.soptions.notifyMsgFiles = domain.notificationmessages.files; } } + if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; } // Add userid command.userid = user._id; @@ -1048,7 +1084,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // If a response is needed, set a callback function var func = null; if (command.responseid != null) { func = function (r) { try { ws.send(JSON.stringify({ action: 'msg', result: r ? 'OK' : 'Unable to route', tag: command.tag, responseid: command.responseid })); } catch (ex) { } } } - + // Route this command to a target node routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions); break; @@ -2132,6 +2168,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use else if (((command.meshtype == 3) || (command.meshtype == 4)) && (parent.args.wanonly == true) && (typeof command.relayid != 'string')) { err = 'Invalid group type'; } // Local device group type wihtout relay is not allowed in WAN mode else if (((command.meshtype == 3) || (command.meshtype == 4)) && (parent.args.lanonly == true) && (typeof command.relayid == 'string')) { err = 'Invalid group type'; } // Local device group type with relay is not allowed in WAN mode else if ((domain.ipkvm == null) && (command.meshtype == 4)) { err = 'Invalid group type'; } // IP KVM device group type is not allowed unless enabled + else if ((command.parent != null) && (typeof command.parent !== 'string' || !parent.meshes[command.parent] || parent.meshes[command.parent].domain !== domain.id)) { err = 'Invalid parent group'; } if ((err == null) && (command.meshtype == 4)) { if ((command.kvmmodel < 1) || (command.kvmmodel > 2)) { err = 'Invalid KVM model'; } else if (common.validateString(command.kvmhost, 1, 128) == false) { err = 'Invalid KVM hostname'; } @@ -2156,6 +2193,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use links[user._id] = { name: user.name, rights: 4294967295 }; mesh = { type: 'mesh', _id: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, domain: domain.id, links: links, creation: Date.now(), creatorid: user._id, creatorname: user.name }; + // Set parent mesh if provided + if (command.parent && parent.meshes[command.parent]) { mesh.parent = command.parent; } + // Add flags and consent if present if (typeof command.flags == 'number') { mesh.flags = command.flags; } if (typeof command.consent == 'number') { mesh.consent = command.consent; } @@ -2845,7 +2885,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use db.Remove('si' + node._id); // Remove system information db.Remove('al' + node._id); // Remove error log last time if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data - db.RemoveAllNodeEvents(node._id); // Remove all events for this node + db.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token db.Get('ra' + node._id, function (err, nodes) { @@ -3730,7 +3770,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var otplib = null; try { otplib = require('otplib'); } catch (ex) { } if (otplib == null) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 6 })); return; } - const secret = otplib.authenticator.generateSecret(); // TODO: Check the random source of this value. + const secret = otplib.generateSecret(); // TODO: Check the random source of this value. var domainName = parent.certificates.CommonName; if (domain.dns != null) { @@ -3738,7 +3778,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } else if (domain.dns == null && domain.id != '') { domainName += "/" + domain.id; } - ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, domainName, secret) })); + ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.generateURI({ issuer: domainName, label: user.name, secret: secret }) })); } break; } @@ -3762,8 +3802,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var otplib = null; try { otplib = require('otplib'); } catch (ex) { } if (otplib == null) { break; } - otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window - if (otplib.authenticator.check(command.token, command.secret) === true) { + const verified = require('otplib').verifySync({ + epochTolerance: 60, + token: command.token, + secret: command.secret, + guardrails: otplib.createGuardrails({ + MIN_SECRET_BYTES: 10, // https://github.com/yeojz/otplib/issues/671#issuecomment-4368647105 + }) + }); + if (verified.valid === true) { // Token is valid, activate 2-step login on this account. user.otpsecret = command.secret; parent.db.SetUser(user); @@ -4329,7 +4376,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } else { // This share is ok, remove extra data we don't need to send. delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.xmeshid; - if (doc.userid != user._id) { delete doc.url; } // If this is not the user who created this link, don't give the link. + if ((user.siteadmin !== SITERIGHT_ADMIN) && (doc.userid != user._id)) { delete doc.url; } // If this is not the user who created this link AND the user is not a site admin, don't give the link. okDocs.push(doc); } } @@ -5264,7 +5311,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (type == 'csv') { try { // Create the CSV file - output = 'id,name,rname,host,icon,ip,osdesc,groupname,av,update,firewall,bitlocker,avdetails,tags,lastbootuptime,cpu,osbuild,biosDate,biosVendor,biosVersion,biosSerial,biosMode,boardName,boardVendor,boardVersion,boardSerial,chassisSerial,chassisAssetTag,chassisManufacturer,productUuid,tpmversion,tpmmanufacturer,tpmmanufacturerversion,tpmisactivated,tpmisenabled,tpmisowned,totalMemory,agentOpenSSL,agentCommitDate,agentCommitHash,agentCompileTime,netIfCount,macs,addresses,lastConnectTime,lastConnectAddr\r\n'; + output = 'id,name,rname,host,icon,ip,osdesc,groupname,av,update,firewall,bitlocker,avdetails,tags,lastbootuptime,cpu,osbuild,arch,kernelRelease,kernelBuild,biosDate,biosVendor,biosVersion,biosSerial,biosMode,boardName,boardVendor,boardVersion,boardSerial,chassisSerial,chassisAssetTag,chassisManufacturer,productUuid,tpmversion,tpmmanufacturer,tpmmanufacturerversion,tpmisactivated,tpmisenabled,tpmisowned,totalMemory,agentOpenSSL,agentCommitDate,agentCommitHash,agentCompileTime,netIfCount,macs,addresses,lastConnectTime,lastConnectAddr\r\n'; for (var i = 0; i < results.length; i++) { const nodeinfo = results[i]; @@ -5274,10 +5321,22 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use output += csvClean(n._id) + ',' + csvClean(n.name) + ',' + csvClean(n.rname ? n.rname : '') + ',' + csvClean(n.host ? n.host : '') + ',' + (n.icon ? n.icon : 1) + ',' + (n.ip ? n.ip : '') + ',' + (n.osdesc ? csvClean(n.osdesc) : '') + ',' + csvClean(parent.meshes[n.meshid].name); if (typeof n.wsc == 'object') { output += ',' + csvClean(n.wsc.antiVirus ? n.wsc.antiVirus : '') + ',' + csvClean(n.wsc.autoUpdate ? n.wsc.autoUpdate : '') + ',' + csvClean(n.wsc.firewall ? n.wsc.firewall : '') + } else if (typeof n.lsc == 'object') { + output += ',' + csvClean(n.lsc.antiVirus ? n.lsc.antiVirus : '') + ',,' + csvClean(n.lsc.firewall ? n.lsc.firewall : '') } else { output += ',,,'; } - if (typeof n.volumes == 'object') { + // BitLocker info sits in the sysinfo node + // present when the user has device-details rights (otherwise sys was deleted above). + var bitlockervolumes = (nodeinfo.sys && nodeinfo.sys.hardware && nodeinfo.sys.hardware.windows) ? nodeinfo.sys.hardware.windows.volumes : null; + if (bitlockervolumes != null) { + // Map the Win32_EncryptableVolume.ConversionStatus codes to labels (legacy string values pass through). var bitlockerdetails = '', firstbitlocker = true; - for (var a in n.volumes) { if (typeof n.volumes[a].protectionStatus !== 'undefined') { if (firstbitlocker) { firstbitlocker = false; } else { bitlockerdetails += '|'; } bitlockerdetails += a + '/' + n.volumes[a].volumeStatus; } } + for (var a in bitlockervolumes) { + var bv = bitlockervolumes[a]; + if ((bv.volumeStatus == null) && (bv.protectionStatus == null)) continue; // no BitLocker info for this volume + if (firstbitlocker) { firstbitlocker = false; } else { bitlockerdetails += '|'; } + var status = (bv.protectionStatus == 2) ? 'Locked' : ((typeof bv.volumeStatus === 'string') ? bv.volumeStatus : (conversionStatus[bv.volumeStatus] || 'Unknown')); + bitlockerdetails += a + '/' + status; + } output += ',' + csvClean(bitlockerdetails); } else { output += ','; @@ -5309,6 +5368,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use output += ','; if (nodeinfo.sys.hardware.windows.osinfo && (nodeinfo.sys.hardware.windows.osinfo.BuildNumber)) { output += csvClean(nodeinfo.sys.hardware.windows.osinfo.BuildNumber); } output += ','; + if (nodeinfo.sys.hardware.windows.osinfo && nodeinfo.sys.hardware.windows.osinfo.OSArchitecture) { output += csvClean(nodeinfo.sys.hardware.windows.osinfo.OSArchitecture); } + output += ','; + output += ','; + output += ','; if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_date)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_date); } output += ','; if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_vendor); } @@ -5364,6 +5427,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use output += ','; output += ','; output += ','; + output += ','; + output += ','; + output += ','; if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.bootloader)) { output += csvClean(nodeinfo.sys.hardware.mobile.bootloader); } output += ','; output += ','; @@ -5390,6 +5456,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use output += ','; if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.cpu_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.cpu_name); } output += ',,'; + if (nodeinfo.sys.hardware.linux && nodeinfo.sys.hardware.linux.arch) { output += csvClean(nodeinfo.sys.hardware.linux.arch); } + output += ','; + if (nodeinfo.sys.hardware.linux && nodeinfo.sys.hardware.linux.kernel_release) { output += csvClean(nodeinfo.sys.hardware.linux.kernel_release); } + output += ','; + if (nodeinfo.sys.hardware.linux && nodeinfo.sys.hardware.linux.kernel_build) { output += csvClean(nodeinfo.sys.hardware.linux.kernel_build); } + output += ','; if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_date)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_date); } output += ','; if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_vendor); } @@ -5442,7 +5514,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } } } else { - output += ',,,,,,,,,,,,,,,,,,,,,,'; + output += ',,,,,,,,,,,,,,,,,,,,,,,,,'; } // Agent information @@ -5501,18 +5573,30 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } } catch (ex) { console.log(ex); } } else { - // Create the JSON file - - // Add the device group name to each device + // Create the JSON file, convert codes into description for (var i = 0; i < results.length; i++) { const nodeinfo = results[i]; if (nodeinfo.node) { const mesh = parent.meshes[nodeinfo.node.meshid]; if (mesh) { results[i].node.groupname = mesh.name; } } + // add a decoded per-volume status summary and remove the raw recovery keys + var bvols = nodeinfo.sys?.hardware?.windows?.volumes; + if (bvols) { + for (var a in bvols) { + var bv = bvols[a]; + if (bv.dType) (bv.dType = driveType[bv.dType] ? driveType[bv.dType] : 'Unknown'); + delete bv.recoveryPassword; // never include raw recovery keys in a report + if ((bv.volumeStatus == null) && (bv.protectionStatus == null)) continue; + if (bv.volumeStatus != null) { bv.volumeStatus = (typeof bv.volumeStatus === 'string') ? bv.volumeStatus : (conversionStatus[bv.volumeStatus] || 'Unknown'); } + if (bv.protectionStatus != null) { bv.protectionStatus = (typeof bv.protectionStatus === 'boolean') ? (bv.protectionStatus ? 'On' : 'Off') : ((typeof bv.protectionStatus === 'string') ? bv.protectionStatus : (protectionStatus[bv.protectionStatus] || 'Unknown')); } + if (bv.encryptionMethod != null) { bv.encryptionMethod = (typeof bv.encryptionMethod === 'string') ? bv.encryptionMethod : (encMethod[bv.encryptionMethod] || 'Unknown'); } + } + delete nodeinfo.sys.hardware.windows.bitlocker; // drop the recovery-key map from the export + } } - output = JSON.stringify(results); + output = JSON.stringify(results, null, 1); } try { ws.send(JSON.stringify({ action: 'getDeviceDetails', data: output, type: type })); } catch (ex) { } }); @@ -5718,6 +5802,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'serverconsole': serverCommandServerConsole, 'servererrors': serverCommandServerErrors, 'serverconfig': serverCommandServerConfig, + 'serverbackuppassword': serverCommandServerBackuppassword, 'serverstats': serverCommandServerStats, 'servertimelinestats': serverCommandServerTimelineStats, 'serverupdate': serverCommandServerUpdate, @@ -6523,6 +6608,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event); } + function serverCommandServerBackuppassword(command) { + if (command.check) { + //check for existing config.json password for requestercheckbox + obj.send({ action: 'backuprestore', data: (Object.hasOwn(parent.parent.config.settings.autobackup, 'zippassword')), dialog: command.dialog }); + } else { + parent.parent.config.settings.autobackup.zippasswordrequest = command.override ? parent.parent.config.settings.autobackup.zippassword : command.password; + } + } + function serverCommandEmailUser(command) { var errMsg = null, emailuser = null; if (domain.mailserver == null) { errMsg = 'Email server not enabled'; } @@ -6621,9 +6715,39 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use delete doc.domain; delete doc._id; - // If this is not a device group admin users, don't send any BitLocker recovery passwords - if ((rights != MESHRIGHT_ADMIN) && (doc.hardware) && (doc.hardware.windows) && (doc.hardware.windows.volumes)) { - for (var i in doc.hardware.windows.volumes) { delete doc.hardware.windows.volumes[i].recoveryPassword; } + // If this is not an admin user, don't send any BitLocker recovery info + if (rights != MESHRIGHT_ADMIN && doc?.hardware?.windows) { + if (doc.hardware.windows?.volumes) { + for (var i in doc.hardware.windows.volumes) { + delete doc.hardware.windows.volumes[i].recoveryPassword; // previous single bitlocker schema + } + } + delete doc.hardware.windows.bitlocker; // new bitlocker driveletter independent cache + } + + // deviceinfo for meshctrl.js, replace raw codes with readable strings and add recoveryPassword if possible + if (command.responseid && command.responseid == 'meshctrl') { + if (doc.hardware?.windows?.volumes) { + for (const [drive, volumeInfo] of Object.entries(doc.hardware.windows.volumes)) { + if (typeof volumeInfo.dType == 'number') { volumeInfo.dType = driveType[volumeInfo.dType] ?? 'Unknown'; } + if (typeof volumeInfo.volumeStatus == 'number') { + if (volumeInfo.volumeStatus == 0) {volumeInfo.volumeStatus = conversionStatus[0]; continue; } + // only do volumes with a encryption status + volumeInfo.volumeStatus = conversionStatus[volumeInfo.volumeStatus] ?? 'Unknown'; + if (volumeInfo.protectionStatus && typeof volumeInfo.protectionStatus == 'number') { volumeInfo.protectionStatus = protectionStatus[volumeInfo.protectionStatus] ?? 'Unknown'; } + if (volumeInfo.encryptionMethod) { volumeInfo.encryptionMethod = encMethod[volumeInfo.encryptionMethod] ?? 'Unknown'; } + if ((rights == MESHRIGHT_ADMIN) && volumeInfo.identifier && !(volumeInfo.hasOwnProperty('recoveryPassword')) && doc.hardware.windows?.bitlocker?.[volumeInfo.identifier]) + { volumeInfo.recoveryPassword = doc.hardware.windows.bitlocker[volumeInfo.identifier].rp; } + } + } + } + var b; + if ((rights == MESHRIGHT_ADMIN) && (b = doc.hardware?.windows?.bitlocker)) { + for (const robj of Object.values(b)) { + robj.recoveryPassword = robj.rp; delete robj.rp; + robj.lastSeen = new Date(robj.t); delete robj.t; + } + } } if (command.nodeinfo === true) { diff --git a/migrate-volume-info.js b/migrate-volume-info.js new file mode 100644 index 0000000000..1679d12fd1 --- /dev/null +++ b/migrate-volume-info.js @@ -0,0 +1,67 @@ +/** +* @description One-shot migration: convert pre-key-list BitLocker sysinfo documents to the +* identifier-keyed hardware.windows.bitlocker map and convert DriveType +* +* Run with: node meshcentral --migrateVolumeInfo +* +* Old sysinfo documents stored BitLocker recovery keys per-volume (recoveryPassword). +* The current schema keeps them in hardware.windows.bitlocker[identifier] = { rp, t }. +* Devices whose agents have not reconnected still hold the old shape. +* This moves the existing keys into the new map. +* Old DriveTypes (cdrom, removable) are converted to the raw windows code +* +* Notes: +* - Idempotent: any document that already has hardware.windows.bitlocker is left untouched, so it is +* safe to run repeatedly and a no-op once every device has re-sent sysinfo. +* - Volume status strings written by the old agent are converted back to their raw codes. +* Any unrecognized string is left untouched and still renders via the views' typeof==='string' branch. +* - The timestamp (t) is taken from the document's own 'time' field (last sysinfo). With the default +* bitlockerKeyRetentionDays=0, migrated keys persist for non-reconnecting devices and are rebuilt +* from the live scan once a device reconnects. +* +* @license Apache-2.0 +*/ +'use strict'; + +// db: the MeshCentral database object (obj.db). callback(err, { scanned, migrated, keysMoved }). +module.exports.migrateVolumeInfo = function (db, callback) { + // Legacy string (old agent stringified values) -> raw code, to restore the schema the current views expect. + var conversionStatusCodes = { 'FullyDecrypted': 0, 'FullyEncrypted': 1, 'EncryptionInProgress': 2, 'DecryptionInProgress': 3, 'EncryptionPaused': 4, 'DecryptionPaused': 5 }; + db.GetAllType('sysinfo', function (err, docs) { + if (err != null) { if (callback != null) { callback(err); } return; } + var scanned = 0, migrated = 0, keysMoved = 0, writesPending = 0, listed = false; + var done = function () { if (listed && (writesPending == 0) && (callback != null)) { callback(null, { scanned: scanned, migrated: migrated, keysMoved: keysMoved }); } }; + for (var d in docs) { + var doc = docs[d]; + scanned++; + var windows = (doc.hardware != null) ? doc.hardware.windows : null; + // Skip non-Windows documents and anything already on the new schema. + if ((windows == null) || (windows.volumes == null) || (windows.bitlocker != null)) { continue; } + + var keys = {}; + for (var letter in windows.volumes) { + var v = windows.volumes[letter]; + // Live key read at the time of the last sysinfo, for the current protector. + if (v.identifier && v.recoveryPassword) { keys[v.identifier] = { rp: v.recoveryPassword, t: (doc.time || 0) }; } + // Reconstruct the raw Win32_LogicalDisk.DriveType code from the legacy boolean flags + if (v.dType == null) { + if (v.removable) { v.dType = 2; } // Removable Disk + else if (v.cdrom) { v.dType = 5; } // Compact Disc / CD-ROM + } + delete v.removable; delete v.cdrom; + // Restore raw status/method codes from the legacy stringified values. Unrecognized strings are left as is, handled in the views + if ((typeof v.volumeStatus === 'string') && (conversionStatusCodes[v.volumeStatus] != null)) { v.volumeStatus = conversionStatusCodes[v.volumeStatus]; } + // Old documents stored protectionStatus as a boolean (true = On; Off/Locked were dropped). Restore the + // raw code: true -> 1 (On). Locked (2) was not retained by the old agent, so it cannot be recovered. + if (typeof v.protectionStatus === 'boolean') { if (v.protectionStatus) { v.protectionStatus = 1; } else { delete v.protectionStatus; } } + } + windows.bitlocker = keys; + migrated++; + keysMoved += Object.keys(keys).length; + writesPending++; + db.Set(doc, function () { writesPending--; done(); }); + } + listed = true; + done(); // handle the case where nothing needed writing + }); +}; diff --git a/mpsserver.js b/mpsserver.js index b7f440e2f7..084643f8ec 100644 --- a/mpsserver.js +++ b/mpsserver.js @@ -439,7 +439,9 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { socket.tag.first = false; // Setup this node with certificate authentication - if (socket.tag.clientCert && socket.tag.clientCert.subject && socket.tag.clientCert.subject.O && socket.tag.clientCert.subject.O.length == 64) { + // Note: getPeerCertificate() returns subject.O as an array when the certificate + // carries repeated O attributes; require a string so the .split() below cannot throw. + if (socket.tag.clientCert && socket.tag.clientCert.subject && (typeof socket.tag.clientCert.subject.O == 'string') && socket.tag.clientCert.subject.O.length == 64) { // This is a node where the MeshID is indicated within the CIRA certificate var domainid = '', meshid; var xx = socket.tag.clientCert.subject.O.split('/'); @@ -679,6 +681,11 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { } } + // A real Intel AMT device always sends PROTOCOLVERSION before USERAUTH. Without it, + // socket.tag.SystemId is undefined and SystemId.split() below (including inside the + // async DNS/DB callbacks) throws uncaught and terminates the server process. + if (typeof socket.tag.SystemId != 'string') { SendUserAuthFail(socket); return -1; } + // If this is a agent-less mesh, use the device guid 3 times as ID. if (initialMesh.mtype == 1) { // Intel AMT GUID (socket.tag.SystemId) will be used as NodeID @@ -1111,7 +1118,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { if (len < 9) return 0; var RecipientChannel = common.ReadInt(data, 1); var LengthOfData = common.ReadInt(data, 5); - if (SourceLen > 1048576) return -1; + if (LengthOfData > 1048576) return -1; if (len < (9 + LengthOfData)) return 0; parent.debug('mpscmddata', '--> CHANNEL_DATA', RecipientChannel, LengthOfData); var cirachannel = socket.tag.channels[RecipientChannel]; diff --git a/package-lock.json b/package-lock.json index 82778fbc1d..9342409975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,38 +1,37 @@ { "name": "meshcentral", - "version": "1.1.57", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meshcentral", - "version": "1.1.57", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "@seald-io/nedb": "4.1.2", "archiver": "7.0.1", - "body-parser": "1.20.4", "cbor": "5.2.0", "compression": "1.8.1", "cookie-session": "2.1.1", - "express": "4.22.1", + "express": "4.22.2", "express-handlebars": "7.1.3", "express-ws": "5.0.2", "ipcheck": "0.1.0", "minimist": "1.2.8", - "multiparty": "4.2.3", - "node-forge": "1.3.2", - "otplib": "12.0.1", + "multiparty": "4.3.0", + "node-forge": "1.4.0", + "otplib": "13.4.1", "ua-client-hints-js": "0.1.2", "ua-parser-js": "1.0.40", - "ws": "8.18.3", + "ws": "8.21.0", "yauzl": "2.10.0" }, "bin": { "meshcentral": "bin/meshcentral" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } }, "node_modules/@isaacs/cliui": { @@ -52,54 +51,72 @@ "node": ">=12" } }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@otplib/core": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz", - "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==", + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-13.4.1.tgz", + "integrity": "sha512-KIXgK1hNtWJEBMTastbe1bpmuais+3f+ATeO8TkMs2rNkfGO1FbQy8+/UWVEu3TR/iTJerU0idkPudaPmLP2BA==", "license": "MIT" }, - "node_modules/@otplib/plugin-crypto": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz", - "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==", - "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "node_modules/@otplib/hotp": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/@otplib/hotp/-/hotp-13.4.1.tgz", + "integrity": "sha512-g9q04SwpG5ZtMnVkUcgcoAlwCH4YLROZN1qhyBwgkBzqYYVSYhpP6gSGaxGHwePLt1c+e6NqDlgIZN+e1/XPuA==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1" + "@otplib/core": "13.4.1", + "@otplib/uri": "13.4.1" } }, - "node_modules/@otplib/plugin-thirty-two": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz", - "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==", - "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "node_modules/@otplib/plugin-base32-scure": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.4.1.tgz", + "integrity": "sha512-Fs/r5qisC05SRhT6xWXaypB6PVC0vgWf6zztmi0J5RnQ09OJiPDWCJFH6cDm6ANsrdvB9di7X+Jb7L13BoEbUA==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "thirty-two": "^1.0.2" + "@otplib/core": "13.4.1", + "@scure/base": "^2.2.0" } }, - "node_modules/@otplib/preset-default": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz", - "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==", - "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths", + "node_modules/@otplib/plugin-crypto-noble": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.4.1.tgz", + "integrity": "sha512-PJfVW8/1hdS6CfxLheKPZSLTwDq4TijZbN4yRjxlv0ODdzmxpM+wGwWr1JXMdy0xJPxLziydQD5gdVqrR4/gAg==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "@otplib/plugin-crypto": "^12.0.1", - "@otplib/plugin-thirty-two": "^12.0.1" + "@noble/hashes": "^2.2.0", + "@otplib/core": "13.4.1" } }, - "node_modules/@otplib/preset-v11": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz", - "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==", + "node_modules/@otplib/totp": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/@otplib/totp/-/totp-13.4.1.tgz", + "integrity": "sha512-QOkBVPrf6AM4qZaReZPSk9/I8ATVdZpIISJz115MqeVtcrbcr5llPZ0J7804tpnjnp1vCRkI5Qjd47HhgVteBQ==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "@otplib/plugin-crypto": "^12.0.1", - "@otplib/plugin-thirty-two": "^12.0.1" + "@otplib/core": "13.4.1", + "@otplib/hotp": "13.4.1", + "@otplib/uri": "13.4.1" + } + }, + "node_modules/@otplib/uri": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/@otplib/uri/-/uri-13.4.1.tgz", + "integrity": "sha512-xaIm7bvICMhoB2rZIR5luiaMdssWR5nY5nXnR1fdezUgZuEO58D6zrGzLp7pQuBmlpmL0HagnscDQFoskp9yiA==", + "license": "MIT", + "dependencies": { + "@otplib/core": "13.4.1" } }, "node_modules/@pkgjs/parseargs": { @@ -112,6 +129,15 @@ "node": ">=14" } }, + "node_modules/@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@seald-io/binary-search-tree": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz", @@ -250,9 +276,9 @@ } }, "node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -270,9 +296,9 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -284,9 +310,9 @@ } }, "node_modules/bare-fs": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", - "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.5.4", @@ -308,37 +334,41 @@ } }, "node_modules/bare-os": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", - "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", "license": "Apache-2.0", "engines": { "bare": ">=1.14.0" } }, "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", "license": "Apache-2.0", "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", - "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", "license": "Apache-2.0", "dependencies": { - "streamx": "^2.21.0", + "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { + "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, "bare-buffer": { "optional": true }, @@ -348,9 +378,9 @@ } }, "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", "license": "Apache-2.0", "dependencies": { "bare-path": "^3.0.0" @@ -386,9 +416,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -399,7 +429,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -410,9 +440,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -461,14 +491,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -813,9 +843,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -867,14 +897,14 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -893,7 +923,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -942,9 +972,9 @@ } }, "node_modules/express-ws/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -1139,9 +1169,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -1199,9 +1229,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1490,9 +1520,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lru-cache": { @@ -1619,12 +1649,12 @@ "license": "MIT" }, "node_modules/multiparty": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz", - "integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.3.0.tgz", + "integrity": "sha512-LD3YDFI9KrDoOGHsPM+hNraPDQQDPLe8Un/kvJfsZCsHKriA4mphg6Ctc2Cuup/59DtHMdAPm6ICXlUmhwTiug==", "license": "MIT", "dependencies": { - "http-errors": "~1.8.1", + "http-errors": "2.0.0", "safe-buffer": "5.2.1", "uid-safe": "2.1.5" }, @@ -1632,38 +1662,29 @@ "node": ">= 0.10" } }, - "node_modules/multiparty/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/multiparty/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { - "depd": "~1.1.2", + "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", + "statuses": "2.0.1", "toidentifier": "1.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/multiparty/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/negotiator": { @@ -1682,9 +1703,9 @@ "license": "MIT" }, "node_modules/node-forge": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", - "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -1742,14 +1763,17 @@ } }, "node_modules/otplib": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz", - "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==", + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-13.4.1.tgz", + "integrity": "sha512-o5CxfDw6bh7hoDv0NUUIcc0RqzJ9ipfUrzeKheKJ+vs4rXZnDlA9n4a/7R1cDjpmLjKLix4BgNVRmoDkm5rLSQ==", "license": "MIT", "dependencies": { - "@otplib/core": "^12.0.1", - "@otplib/preset-default": "^12.0.1", - "@otplib/preset-v11": "^12.0.1" + "@otplib/core": "13.4.1", + "@otplib/hotp": "13.4.1", + "@otplib/plugin-base32-scure": "13.4.1", + "@otplib/plugin-crypto-noble": "13.4.1", + "@otplib/totp": "13.4.1", + "@otplib/uri": "13.4.1" } }, "node_modules/package-json-from-dist": { @@ -1793,9 +1817,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/pend": { @@ -1842,9 +1866,9 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -2059,14 +2083,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -2078,13 +2102,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -2161,9 +2185,9 @@ } }, "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.27.0.tgz", + "integrity": "sha512-WZ189TKnHoAokYHvwzaAQMpd55cgUmFIcJFzBSgGcb886jau5DL+XdDhTWV4ps3FLvk+OORp0dLRTPsLZ21CSA==", "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -2277,9 +2301,9 @@ } }, "node_modules/tar-stream": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", - "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -2306,14 +2330,6 @@ "b4a": "^1.6.4" } }, - "node_modules/thirty-two": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz", - "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==", - "engines": { - "node": ">=0.2.6" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2478,13 +2494,13 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -2596,9 +2612,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index c3b6c25746..a4b3e73484 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "1.1.57", + "version": "1.2.1", "keywords": [ "Remote Device Management", "Remote Device Monitoring", @@ -38,26 +38,26 @@ ], "dependencies": { "@seald-io/nedb": "4.1.2", + "@zip.js/zip.js": "2.8.26", "archiver": "7.0.1", - "body-parser": "1.20.4", "cbor": "5.2.0", "compression": "1.8.1", "cookie-session": "2.1.1", - "express": "4.22.1", + "express": "4.22.2", "express-handlebars": "7.1.3", "express-ws": "5.0.2", "ipcheck": "0.1.0", "minimist": "1.2.8", - "multiparty": "4.2.3", - "node-forge": "1.3.2", - "otplib": "12.0.1", + "multiparty": "4.3.0", + "node-forge": "1.4.0", + "otplib": "13.4.1", "ua-client-hints-js": "0.1.2", "ua-parser-js": "1.0.40", - "ws": "8.18.3", + "ws": "8.21.0", "yauzl": "2.10.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "repository": { "type": "git", diff --git a/pluginHandler.js b/pluginHandler.js index 3b7ba2051c..fdc69cd453 100644 --- a/pluginHandler.js +++ b/pluginHandler.js @@ -251,9 +251,10 @@ module.exports.pluginHandler = function (parent) { return new Promise(function (resolve, reject) { var http = (configUrl.indexOf('https://') >= 0) ? require('https') : require('http'); if (configUrl.indexOf('://') === -1) reject("Unable to fetch the config: Bad URL (" + configUrl + ")"); - var options = require('url').parse(configUrl); + const getme = new URL(configUrl); + var options = { protocol: getme.protocol, hostname: getme.hostname, port: getme.port || undefined, path: getme.pathname + getme.search }; if (typeof parent.config.settings.plugins.proxy == 'string' || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) { // Proxy support - options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(require('url').parse(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']); + options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(new URL(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']); } http.get(options, function (res) { var configStr = ''; @@ -422,8 +423,7 @@ module.exports.pluginHandler = function (parent) { var dl_url = plugin.downloadUrl; if (version_only != null && version_only != false) dl_url = version_only.url; if (force_url != null) dl_url = force_url; - var url = require('url'); - var q = url.parse(dl_url, true); + var q = new URL(dl_url); var http = (q.protocol == "http:") ? require('http') : require('https'); var opts = { path: q.pathname, @@ -436,7 +436,7 @@ module.exports.pluginHandler = function (parent) { method: 'GET' }; if (typeof parent.config.settings.plugins.proxy == 'string' || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) { // Proxy support - opts.agent = new (require('https-proxy-agent').HttpsProxyAgent)(require('url').parse(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']); + opts.agent = new (require('https-proxy-agent').HttpsProxyAgent)(new URL(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']); } var done = false; var request = http.get(opts, function (response) { @@ -522,8 +522,7 @@ module.exports.pluginHandler = function (parent) { parent.db.getPlugin(id, function (err, docs) { var plugin = docs[0]; if (plugin.versionHistoryUrl == null) reject("No version history available for this plugin."); - var url = require('url'); - var q = url.parse(plugin.versionHistoryUrl, true); + var q = new URL(plugin.versionHistoryUrl); var http = (q.protocol == 'http:') ? require('http') : require('https'); var opts = { path: q.pathname, @@ -535,7 +534,7 @@ module.exports.pluginHandler = function (parent) { } }; if (typeof parent.config.settings.plugins.proxy == 'string' || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) { // Proxy support - options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(require('url').parse(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']); + options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(new URL(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']); } http.get(opts, function (res) { var versStr = ''; diff --git a/public/commander-de.htm b/public/commander-de.htm index 91a57400c3..13fdbcab22 100644 --- a/public/commander-de.htm +++ b/public/commander-de.htm @@ -1,8 +1,8 @@ -
  
Getrennt