From 135a1db932eaf8e85f97c49b466cdceb9bf0ba04 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Fri, 7 Feb 2025 19:51:53 +0000 Subject: [PATCH] Adapter was rewritten in TypeScript --- .eslintrc.json | 6 - .github/dependabot.yml | 30 +- .github/stale.yml | 49 +- .github/workflows/test-and-release.yml | 284 +- .mocharc.json | 4 +- .releaseconfig.json | 8 +- LICENSE | 2 +- README.md | 29 +- admin/i18n/{de/translations.json => de.json} | 24 +- admin/i18n/{en/translations.json => en.json} | 24 +- admin/i18n/{es/translations.json => es.json} | 24 +- admin/i18n/{fr/translations.json => fr.json} | 24 +- admin/i18n/{it/translations.json => it.json} | 24 +- admin/i18n/{nl/translations.json => nl.json} | 24 +- admin/i18n/{pl/translations.json => pl.json} | 24 +- admin/i18n/{pt/translations.json => pt.json} | 24 +- admin/i18n/{ru/translations.json => ru.json} | 24 +- admin/i18n/{uk/translations.json => uk.json} | 2 +- .../{zh-cn/translations.json => zh-cn.json} | 24 +- admin/jsonConfig.json | 254 +- admin/ws.png | Bin 1423 -> 0 bytes admin/ws.svg | 3 + {lib => dist/lib}/socket.io.js | 0 dist/lib/socketWS.d.ts | 22 + dist/lib/socketWS.js | 220 ++ dist/lib/socketWS.js.map | 1 + dist/main.d.ts | 18 + dist/main.js | 300 +++ dist/main.js.map | 1 + eslint.config.mjs | 26 + example/conn.js | 3 +- example/index.html | 99 +- example/socket-client/Connection.d.ts | 676 +++++ example/socket-client/Connection.js | 2290 +++++++++++++++++ example/socket-client/ConnectionProps.d.ts | 55 + example/socket-client/ConnectionProps.js | 2 + example/socket-client/DeferredPromise.d.ts | 5 + example/socket-client/DeferredPromise.js | 12 + example/socket-client/README.md | 4 + example/socket-client/globals.d.ts | 17 + example/socket-client/globals.js | 2 + example/socket-client/tools.d.ts | 12 + example/socket-client/tools.js | 33 + io-package.json | 412 ++- lib/passportSocket.js | 100 - lib/socket.js | 37 - lib/socketCommands.js | 1682 ------------ lib/socketCommon.js | 435 ---- lib/socketWS.js | 218 -- main.js | 290 --- package.json | 127 +- prettier.config.js | 1 - prettier.config.mjs | 3 + src/lib/socketWS.ts | 285 ++ src/main.ts | 326 +++ src/types.d.ts | 14 + tasks.js | 4 +- test/mocha.setup.js | 4 +- test/testAdapter.js | 63 +- tsconfig.build.json | 8 + tsconfig.json | 30 + 61 files changed, 5150 insertions(+), 3598 deletions(-) delete mode 100644 .eslintrc.json rename admin/i18n/{de/translations.json => de.json} (96%) rename admin/i18n/{en/translations.json => en.json} (97%) rename admin/i18n/{es/translations.json => es.json} (96%) rename admin/i18n/{fr/translations.json => fr.json} (94%) rename admin/i18n/{it/translations.json => it.json} (95%) rename admin/i18n/{nl/translations.json => nl.json} (95%) rename admin/i18n/{pl/translations.json => pl.json} (95%) rename admin/i18n/{pt/translations.json => pt.json} (96%) rename admin/i18n/{ru/translations.json => ru.json} (94%) rename admin/i18n/{uk/translations.json => uk.json} (99%) rename admin/i18n/{zh-cn/translations.json => zh-cn.json} (92%) delete mode 100644 admin/ws.png create mode 100644 admin/ws.svg rename {lib => dist/lib}/socket.io.js (100%) create mode 100644 dist/lib/socketWS.d.ts create mode 100644 dist/lib/socketWS.js create mode 100644 dist/lib/socketWS.js.map create mode 100644 dist/main.d.ts create mode 100644 dist/main.js create mode 100644 dist/main.js.map create mode 100644 eslint.config.mjs create mode 100644 example/socket-client/Connection.d.ts create mode 100644 example/socket-client/Connection.js create mode 100644 example/socket-client/ConnectionProps.d.ts create mode 100644 example/socket-client/ConnectionProps.js create mode 100644 example/socket-client/DeferredPromise.d.ts create mode 100644 example/socket-client/DeferredPromise.js create mode 100644 example/socket-client/README.md create mode 100644 example/socket-client/globals.d.ts create mode 100644 example/socket-client/globals.js create mode 100644 example/socket-client/tools.d.ts create mode 100644 example/socket-client/tools.js delete mode 100644 lib/passportSocket.js delete mode 100644 lib/socket.js delete mode 100644 lib/socketCommands.js delete mode 100644 lib/socketCommon.js delete mode 100644 lib/socketWS.js delete mode 100644 main.js delete mode 100644 prettier.config.js create mode 100644 prettier.config.mjs create mode 100644 src/lib/socketWS.ts create mode 100644 src/main.ts create mode 100644 src/types.d.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3acb45c..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": ["@foxriver76/eslint-config"], - "rules": { - "unicorn/prefer-module": 0 - } -} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 346fe51..1d6be8a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,17 +1,17 @@ version: 2 updates: - # Maintain dependencies for GitHub Actions - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - time: "04:00" - timezone: Europe/Berlin - - package-ecosystem: npm - directory: "/" - schedule: - interval: monthly - time: "04:00" - timezone: Europe/Berlin - open-pull-requests-limit: 5 - versioning-strategy: increase + # Maintain dependencies for GitHub Actions + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: monthly + time: '04:00' + timezone: Europe/Berlin + - package-ecosystem: npm + directory: '/' + schedule: + interval: monthly + time: '04:00' + timezone: Europe/Berlin + open-pull-requests-limit: 5 + versioning-strategy: increase diff --git a/.github/stale.yml b/.github/stale.yml index 8a1bc21..c8cf3a3 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -12,9 +12,9 @@ onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - - enhancement - - security - - bug + - enhancement + - security + - bug # Set to true to ignore issues in a project (defaults to false) exemptProjects: true @@ -30,19 +30,19 @@ staleLabel: wontfix # Comment to post when marking as stale. Set to `false` to disable markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs within the next 7 days. - Please check if the issue is still relevant in the most current version of the adapter - and tell us. Also check that all relevant details, logs and reproduction steps - are included and update them if needed. - Thank you for your contributions. - - Dieses Problem wurde automatisch als veraltet markiert, da es in letzter Zeit keine Aktivitäten gab. - Es wird geschlossen, wenn nicht innerhalb der nächsten 7 Tage weitere Aktivitäten stattfinden. - Bitte überprüft, ob das Problem auch in der aktuellsten Version des Adapters noch relevant ist, - und teilt uns dies mit. Überprüft auch, ob alle relevanten Details, Logs und Reproduktionsschritte - enthalten sind bzw. aktualisiert diese. - Vielen Dank für Eure Unterstützung. + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs within the next 7 days. + Please check if the issue is still relevant in the most current version of the adapter + and tell us. Also check that all relevant details, logs and reproduction steps + are included and update them if needed. + Thank you for your contributions. + + Dieses Problem wurde automatisch als veraltet markiert, da es in letzter Zeit keine Aktivitäten gab. + Es wird geschlossen, wenn nicht innerhalb der nächsten 7 Tage weitere Aktivitäten stattfinden. + Bitte überprüft, ob das Problem auch in der aktuellsten Version des Adapters noch relevant ist, + und teilt uns dies mit. Überprüft auch, ob alle relevanten Details, Logs und Reproduktionsschritte + enthalten sind bzw. aktualisiert diese. + Vielen Dank für Eure Unterstützung. # Comment to post when removing the stale label. # unmarkComment: > @@ -50,22 +50,21 @@ markComment: > # Comment to post when closing a stale Issue or Pull Request. closeComment: > - This issue has been automatically closed because of inactivity. Please open a new - issue if still relevant and make sure to include all relevant details, logs and - reproduction steps. - Thank you for your contributions. + This issue has been automatically closed because of inactivity. Please open a new + issue if still relevant and make sure to include all relevant details, logs and + reproduction steps. + Thank you for your contributions. - Dieses Problem wurde aufgrund von Inaktivität automatisch geschlossen. Bitte öffnet ein - neues Issue, falls dies noch relevant ist und stellt sicher das alle relevanten Details, - Logs und Reproduktionsschritte enthalten sind. - Vielen Dank für Eure Unterstützung. + Dieses Problem wurde aufgrund von Inaktivität automatisch geschlossen. Bitte öffnet ein + neues Issue, falls dies noch relevant ist und stellt sicher das alle relevanten Details, + Logs und Reproduktionsschritte enthalten sind. + Vielen Dank für Eure Unterstützung. # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 # Limit to only `issues` or `pulls` only: issues - # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': # pulls: # daysUntilStale: 30 diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index a349bc8..283d323 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -6,151 +6,149 @@ name: Test and Release # Run this job on all pushes and pull requests # as well as tags with a semantic version on: - push: - branches: - - '*' - tags: - # normal versions - - "v?[0-9]+.[0-9]+.[0-9]+" - # pre-releases - - "v?[0-9]+.[0-9]+.[0-9]+-**" - pull_request: {} + push: + branches: + - '*' + tags: + # normal versions + - 'v?[0-9]+.[0-9]+.[0-9]+' + # pre-releases + - 'v?[0-9]+.[0-9]+.[0-9]+-**' + pull_request: {} # Cancel previous PR/branch runs when a new commit is pushed concurrency: - group: ${{ github.ref }} - cancel-in-progress: true + group: ${{ github.ref }} + cancel-in-progress: true jobs: - # Performs quick checks before the expensive test runs - check-and-lint: - if: contains(github.event.head_commit.message, '[skip ci]') == false - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18.x - - - - name: Install Dependencies - run: npm install - -# - name: Perform a type check -# run: npm run check:ts -# env: -# CI: true - # - name: Lint TypeScript code - # run: npm run lint -# - name: Test package files -# run: npm run test:package - - # Runs adapter tests on all supported node versions and OSes - adapter-tests: - if: contains(github.event.head_commit.message, '[skip ci]') == false - - needs: [check-and-lint] - - runs-on: ${{ matrix.os }} - strategy: - matrix: - node-version: [18.x, 20.x] - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Install Dependencies - run: npm install - - - name: Run local tests - run: npm test -# - name: Run unit tests -# run: npm run test:unit -# - name: Run integration tests # (linux/osx) -# if: startsWith(runner.OS, 'windows') == false -# run: DEBUG=testing:* npm run test:integration -# - name: Run integration tests # (windows) -# if: startsWith(runner.OS, 'windows') -# run: set DEBUG=testing:* & npm run test:integration - - # Deploys the final package to NPM - deploy: - needs: [adapter-tests] - - # Trigger this step only when a commit on master is tagged with a version number - if: | - contains(github.event.head_commit.message, '[skip ci]') == false && - github.event_name == 'push' && - startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Use Node.js 18.x - uses: actions/setup-node@v4 - with: - node-version: 18.x - - - name: Extract the version and commit body from the tag - id: extract_release - # The body may be multiline, therefore we need to escape some characters - run: | - VERSION="${{ github.ref }}" - VERSION=${VERSION##*/} - VERSION=${VERSION##*v} - echo "::set-output name=VERSION::$VERSION" - BODY=$(git show -s --format=%b) - BODY="${BODY//'%'/'%25'}" - BODY="${BODY//$'\n'/'%0A'}" - BODY="${BODY//$'\r'/'%0D'}" - echo "::set-output name=BODY::$BODY" - - - name: Install Dependencies - run: npm install - -# - name: Create a clean build -# run: npm run build - - name: Publish package to npm - run: | - npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} - npm whoami - npm publish - - - name: Create Github Release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release v${{ steps.extract_release.outputs.VERSION }} - draft: false - # Prerelease versions create pre-releases on GitHub - prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }} - body: ${{ steps.extract_release.outputs.BODY }} - - - name: Notify Sentry.io about the release - run: | - npm i -g @sentry/cli - export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} - export SENTRY_URL=https://sentry.iobroker.net - export SENTRY_ORG=iobroker - export SENTRY_PROJECT=iobroker-ws - export SENTRY_VERSION=iobroker.ws@${{ steps.extract_release.outputs.VERSION }} - sentry-cli releases new $SENTRY_VERSION - sentry-cli releases finalize $SENTRY_VERSION - - # Add the following line BEFORE finalize if repositories are connected in Sentry - # sentry-cli releases set-commits $SENTRY_VERSION --auto - - # Add the following line BEFORE finalize if sourcemap uploads are needed - # sentry-cli releases files $SENTRY_VERSION upload-sourcemaps build/ + # Performs quick checks before the expensive test runs + check-and-lint: + if: contains(github.event.head_commit.message, '[skip ci]') == false + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Install Dependencies + run: npm install + + # - name: Perform a type check + # run: npm run check:ts + # env: + # CI: true + # - name: Lint TypeScript code + # run: npm run lint + # - name: Test package files + # run: npm run test:package + + # Runs adapter tests on all supported node versions and OSes + adapter-tests: + if: contains(github.event.head_commit.message, '[skip ci]') == false + + needs: [check-and-lint] + + runs-on: ${{ matrix.os }} + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Dependencies + run: npm install + + - name: Run local tests + run: npm test + # - name: Run unit tests + # run: npm run test:unit + # - name: Run integration tests # (linux/osx) + # if: startsWith(runner.OS, 'windows') == false + # run: DEBUG=testing:* npm run test:integration + # - name: Run integration tests # (windows) + # if: startsWith(runner.OS, 'windows') + # run: set DEBUG=testing:* & npm run test:integration + + # Deploys the final package to NPM + deploy: + needs: [adapter-tests] + + # Trigger this step only when a commit on master is tagged with a version number + if: | + contains(github.event.head_commit.message, '[skip ci]') == false && + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + + - name: Extract the version and commit body from the tag + id: extract_release + # The body may be multiline, therefore we need to escape some characters + run: | + VERSION="${{ github.ref }}" + VERSION=${VERSION##*/} + VERSION=${VERSION##*v} + echo "::set-output name=VERSION::$VERSION" + BODY=$(git show -s --format=%b) + BODY="${BODY//'%'/'%25'}" + BODY="${BODY//$'\n'/'%0A'}" + BODY="${BODY//$'\r'/'%0D'}" + echo "::set-output name=BODY::$BODY" + + - name: Install Dependencies + run: npm install + + # - name: Create a clean build + # run: npm run build + - name: Publish package to npm + run: | + npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} + npm whoami + npm publish + + - name: Create Github Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release v${{ steps.extract_release.outputs.VERSION }} + draft: false + # Prerelease versions create pre-releases on GitHub + prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }} + body: ${{ steps.extract_release.outputs.BODY }} + + - name: Notify Sentry.io about the release + run: | + npm i -g @sentry/cli + export SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} + export SENTRY_URL=https://sentry.iobroker.net + export SENTRY_ORG=iobroker + export SENTRY_PROJECT=iobroker-ws + export SENTRY_VERSION=iobroker.ws@${{ steps.extract_release.outputs.VERSION }} + sentry-cli releases new $SENTRY_VERSION + sentry-cli releases finalize $SENTRY_VERSION + + # Add the following line BEFORE finalize if repositories are connected in Sentry + # sentry-cli releases set-commits $SENTRY_VERSION --auto + + # Add the following line BEFORE finalize if sourcemap uploads are needed + # sentry-cli releases files $SENTRY_VERSION upload-sourcemaps build/ diff --git a/.mocharc.json b/.mocharc.json index 89a1352..6a2d8be 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,5 +1,3 @@ { - "require": [ - "./test/mocha.setup.js" - ] + "require": ["./test/mocha.setup.js"] } diff --git a/.releaseconfig.json b/.releaseconfig.json index 3ce9936..65cb56b 100644 --- a/.releaseconfig.json +++ b/.releaseconfig.json @@ -1,6 +1,6 @@ { - "plugins": ["iobroker", "license"], - "exec": { - "before_commit": "npm run build" - } + "plugins": ["iobroker", "license"], + "exec": { + "before_commit": "npm run build" + } } diff --git a/LICENSE b/LICENSE index 1531229..718c6eb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2024 bluefox +Copyright (c) 2014-2025 bluefox Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f98f461..48d97e3 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,14 @@ It is suggested to use [socket class](https://github.com/ioBroker/socket-client) --> ## Changelog +### **WORK IN PROGRESS** +* (@GermanBluefox) Adapter was rewritten in TypeScript + ### 2.7.0 (2024-11-17) -* (bluefox) Update ws-server library +* (@GermanBluefox) Update ws-server library ### 2.6.2 (2024-06-26) -* (bluefox) Corrected call of getObjectView with null parameter +* (@GermanBluefox) Corrected call of getObjectView with null parameter ### 2.6.1 (2024-04-22) * (foxriver76) fixed require of webserver @@ -82,7 +85,7 @@ It is suggested to use [socket class](https://github.com/ioBroker/socket-client) * (foxriver76) use `@iobroker/webserver` ### 2.5.11 (2024-02-22) -* (bluefox) Some packages were updated +* (@GermanBluefox) Some packages were updated ### 2.5.10 (2023-12-17) * (foxriver76) updated ws-server to increase the file limit to 500 MB @@ -91,13 +94,13 @@ It is suggested to use [socket class](https://github.com/ioBroker/socket-client) * (joltcoke) Corrected the crash if authentication is enabled ### 2.5.8 (2023-10-11) -* (bluefox) Corrected adapter termination if the alias has no target +* (@GermanBluefox) Corrected adapter termination if the alias has no target ### 2.5.7 (2023-10-07) * (foxriver76) upgraded socket-classes to fix vis problems ### 2.5.6 (2023-09-28) -* (bluefox) upgraded socket-classes to correct the error by unsubscribing on client disconnect +* (@GermanBluefox) upgraded socket-classes to correct the error by unsubscribing on client disconnect ### 2.5.5 (2023-09-14) * (foxriver76) upgraded socket-classes to fix crash cases @@ -106,27 +109,27 @@ It is suggested to use [socket class](https://github.com/ioBroker/socket-client) * (mcm1957) added missing node16 requirement ### 2.5.3 (2023-08-01) -* (bluefox) Added the subscribing on the specific instance messages +* (@GermanBluefox) Added the subscribing on the specific instance messages ### 2.4.0 (2023-07-07) -* (bluefox) extended the getObjects function with the possibility to read the list of IDs +* (@GermanBluefox) extended the getObjects function with the possibility to read the list of IDs ### 2.3.6 (2023-03-03) -* (bluefox) Allowed deletion of fullcalendar objects +* (@GermanBluefox) Allowed deletion of fullcalendar objects ### 2.3.5 (2023-01-29) -* (bluefox) added `publishFileAll` method (for future use) +* (@GermanBluefox) added `publishFileAll` method (for future use) ### 2.3.4 (2022-12-27) -* (bluefox) corrected connection string +* (@GermanBluefox) corrected connection string ### 2.3.3 (2022-12-22) -* (bluefox) used new socket-classes +* (@GermanBluefox) used new socket-classes ### 2.3.1 (2022-11-27) -* (bluefox) Added `fileChange` event +* (@GermanBluefox) Added `fileChange` event ## License The MIT License (MIT) -Copyright (c) 2014-2024 bluefox +Copyright (c) 2014-2025 @GermanBluefox diff --git a/admin/i18n/de/translations.json b/admin/i18n/de.json similarity index 96% rename from admin/i18n/de/translations.json rename to admin/i18n/de.json index 29ef9ee..1e9314c 100644 --- a/admin/i18n/de/translations.json +++ b/admin/i18n/de.json @@ -1,20 +1,20 @@ { - "Run as": "Laufen unter Anwender", - "IP": "IP", - "Port": "Port", - "Secure(HTTPS)": "Verschlüsselung(HTTPS)", "Authentication": "Authentifizierung", - "Listen on all IPs": "An allen IP Adressen hören", - "help_tip": "Beim Speichern von Einstellungen der Adapter wird sofort neu gestartet.", - "Public certificate": "Publikzertifikat", - "Private certificate": "Privatzertifikat", "Chained certificate": "Kettenzertifikat", + "IP": "IP", + "Language for only this instance": "Sprache nur für diese Instanz", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Einstellungen Let's Encrypt", - "Use Lets Encrypt certificates": "Benutzen Let's Encrypt Zertifikate", - "Use this instance for automatic update": "Benutze diese Instanz für automatische Updates", + "Listen on all IPs": "An allen IP Adressen hören", + "Port": "Port", "Port to check the domain": "Port um die Domain zu prüfen", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Privatzertifikat", + "Public certificate": "Publikzertifikat", "Read about Let's Encrypt certificates": "Lesen Sie mehr über Let's Encrypt-Zertifikate", + "Run as": "Laufen unter Anwender", + "Secure(HTTPS)": "Verschlüsselung(HTTPS)", "Undo": "Rückgängig", - "Language for only this instance": "Sprache nur für diese Instanz" + "Use Lets Encrypt certificates": "Benutzen Let's Encrypt Zertifikate", + "Use this instance for automatic update": "Benutze diese Instanz für automatische Updates", + "help_tip": "Beim Speichern von Einstellungen der Adapter wird sofort neu gestartet." } \ No newline at end of file diff --git a/admin/i18n/en/translations.json b/admin/i18n/en.json similarity index 97% rename from admin/i18n/en/translations.json rename to admin/i18n/en.json index 6e00032..f89ff8b 100644 --- a/admin/i18n/en/translations.json +++ b/admin/i18n/en.json @@ -1,20 +1,20 @@ { - "Run as": "Run as", - "IP": "IP", - "Port": "Port", - "Secure(HTTPS)": "Secure(HTTPS)", "Authentication": "Authentication", - "Listen on all IPs": "Listen on all IPs", - "help_tip": "On save the adapter restarts with new configuration immediately", - "Public certificate": "Public certificate", - "Private certificate": "Private certificate", "Chained certificate": "Chained certificate", + "IP": "IP", + "Language for only this instance": "Language for only this instance", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Let's Encrypt settings", - "Use Lets Encrypt certificates": "Use Let's Encrypt certificates", - "Use this instance for automatic update": "Use this instance for automatic update", + "Listen on all IPs": "Listen on all IPs", + "Port": "Port", "Port to check the domain": "Port to check the domain", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Private certificate", + "Public certificate": "Public certificate", "Read about Let's Encrypt certificates": "Read about Let's Encrypt certificates", + "Run as": "Run as", + "Secure(HTTPS)": "Secure(HTTPS)", "Undo": "Undo", - "Language for only this instance": "Language for only this instance" + "Use Lets Encrypt certificates": "Use Let's Encrypt certificates", + "Use this instance for automatic update": "Use this instance for automatic update", + "help_tip": "On save the adapter restarts with new configuration immediately" } \ No newline at end of file diff --git a/admin/i18n/es/translations.json b/admin/i18n/es.json similarity index 96% rename from admin/i18n/es/translations.json rename to admin/i18n/es.json index 01dfab1..8e4e942 100644 --- a/admin/i18n/es/translations.json +++ b/admin/i18n/es.json @@ -1,20 +1,20 @@ { - "Run as": "Correr como", - "IP": "IP", - "Port": "Puerto", - "Secure(HTTPS)": "Seguro (HTTPS)", "Authentication": "Autenticación", - "Listen on all IPs": "Escuchar en todas las direcciones IP", - "help_tip": "Al guardar, el adaptador se reinicia con una nueva configuración de inmediato", - "Public certificate": "Certificado público", - "Private certificate": "Certificado privado", "Chained certificate": "Certificado encadenado", + "IP": "IP", + "Language for only this instance": "Idioma solo para esta instancia", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Vamos a cifrar la configuración", - "Use Lets Encrypt certificates": "Utilice los certificados Let's Encrypt", - "Use this instance for automatic update": "Use esta instancia para la actualización automática", + "Listen on all IPs": "Escuchar en todas las direcciones IP", + "Port": "Puerto", "Port to check the domain": "Puerto para verificar el dominio", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Certificado privado", + "Public certificate": "Certificado público", "Read about Let's Encrypt certificates": "Lea acerca de los certificados de Let's Encrypt", + "Run as": "Correr como", + "Secure(HTTPS)": "Seguro (HTTPS)", "Undo": "Deshacer", - "Language for only this instance": "Idioma solo para esta instancia" + "Use Lets Encrypt certificates": "Utilice los certificados Let's Encrypt", + "Use this instance for automatic update": "Use esta instancia para la actualización automática", + "help_tip": "Al guardar, el adaptador se reinicia con una nueva configuración de inmediato" } \ No newline at end of file diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr.json similarity index 94% rename from admin/i18n/fr/translations.json rename to admin/i18n/fr.json index 9901880..acc9476 100644 --- a/admin/i18n/fr/translations.json +++ b/admin/i18n/fr.json @@ -1,20 +1,20 @@ { - "Run as": "Courir comme", - "IP": "IP", - "Port": "Port", - "Secure(HTTPS)": "Sécurisé (HTTPS)", "Authentication": "Authentification", - "Listen on all IPs": "Écoutez sur toutes les adresses IP", - "help_tip": "Sur enregistrer l'adaptateur redémarre avec la nouvelle configuration immédiatement", - "Public certificate": "Certificat public", - "Private certificate": "Certificat privé", "Chained certificate": "Certificat chaîné", + "IP": "IP", + "Language for only this instance": "Langue pour cette instance uniquement", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Cryptons les paramètres", - "Use Lets Encrypt certificates": "Utiliser les certificats Let's Encrypt", - "Use this instance for automatic update": "Utilisez cette instance pour la mise à jour automatique", + "Listen on all IPs": "Écoutez sur toutes les adresses IP", + "Port": "Port", "Port to check the domain": "Port pour vérifier le domaine", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Certificat privé", + "Public certificate": "Certificat public", "Read about Let's Encrypt certificates": "En savoir plus sur les certificats Let's Encrypt", + "Run as": "Courir comme", + "Secure(HTTPS)": "Sécurisé (HTTPS)", "Undo": "annuler", - "Language for only this instance": "Langue pour cette instance uniquement" + "Use Lets Encrypt certificates": "Utiliser les certificats Let's Encrypt", + "Use this instance for automatic update": "Utilisez cette instance pour la mise à jour automatique", + "help_tip": "Sur enregistrer l'adaptateur redémarre avec la nouvelle configuration immédiatement" } \ No newline at end of file diff --git a/admin/i18n/it/translations.json b/admin/i18n/it.json similarity index 95% rename from admin/i18n/it/translations.json rename to admin/i18n/it.json index 2f3b947..65104dd 100644 --- a/admin/i18n/it/translations.json +++ b/admin/i18n/it.json @@ -1,20 +1,20 @@ { - "Run as": "Correre come", - "IP": "IP", - "Port": "Porta", - "Secure(HTTPS)": "Sicuro (HTTPS)", "Authentication": "Autenticazione", - "Listen on all IPs": "Ascolta su tutti gli IP", - "help_tip": "Al salvataggio, l'adattatore si riavvia immediatamente con la nuova configurazione", - "Public certificate": "Certificato pubblico", - "Private certificate": "Certificato privato", "Chained certificate": "Certificato incatenato", + "IP": "IP", + "Language for only this instance": "Linguaggio solo per questa istanza", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Let's Encrypt settings", - "Use Lets Encrypt certificates": "Utilizza Let's Encrypt certificates", - "Use this instance for automatic update": "Utilizza questa istanza per l'aggiornamento automatico", + "Listen on all IPs": "Ascolta su tutti gli IP", + "Port": "Porta", "Port to check the domain": "Porta per controllare il dominio", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Certificato privato", + "Public certificate": "Certificato pubblico", "Read about Let's Encrypt certificates": "Leggi i certificati Let's Encrypt", + "Run as": "Correre come", + "Secure(HTTPS)": "Sicuro (HTTPS)", "Undo": "Annullare", - "Language for only this instance": "Linguaggio solo per questa istanza" + "Use Lets Encrypt certificates": "Utilizza Let's Encrypt certificates", + "Use this instance for automatic update": "Utilizza questa istanza per l'aggiornamento automatico", + "help_tip": "Al salvataggio, l'adattatore si riavvia immediatamente con la nuova configurazione" } \ No newline at end of file diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl.json similarity index 95% rename from admin/i18n/nl/translations.json rename to admin/i18n/nl.json index 4240949..a737ae9 100644 --- a/admin/i18n/nl/translations.json +++ b/admin/i18n/nl.json @@ -1,20 +1,20 @@ { - "Run as": "Rennen als", - "IP": "IK P", - "Port": "Haven", - "Secure(HTTPS)": "Secure (HTTPS)", "Authentication": "authenticatie", - "Listen on all IPs": "Luister op alle IP's", - "help_tip": "Bij opslaan wordt de adapter onmiddellijk opnieuw opgestart met een nieuwe configuratie", - "Public certificate": "Openbaar certificaat", - "Private certificate": "Privé certificaat", "Chained certificate": "Geketend certificaat", + "IP": "IK P", + "Language for only this instance": "Taal voor alleen deze instantie", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Laten we de instellingen versleutelen", - "Use Lets Encrypt certificates": "Gebruik Let's Encrypt-certificaten", - "Use this instance for automatic update": "Gebruik deze instantie voor automatische update", + "Listen on all IPs": "Luister op alle IP's", + "Port": "Haven", "Port to check the domain": "Poort om het domein te controleren", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Privé certificaat", + "Public certificate": "Openbaar certificaat", "Read about Let's Encrypt certificates": "Lees over Let's Encrypt-certificaten", + "Run as": "Rennen als", + "Secure(HTTPS)": "Secure (HTTPS)", "Undo": "ongedaan maken", - "Language for only this instance": "Taal voor alleen deze instantie" + "Use Lets Encrypt certificates": "Gebruik Let's Encrypt-certificaten", + "Use this instance for automatic update": "Gebruik deze instantie voor automatische update", + "help_tip": "Bij opslaan wordt de adapter onmiddellijk opnieuw opgestart met een nieuwe configuratie" } \ No newline at end of file diff --git a/admin/i18n/pl/translations.json b/admin/i18n/pl.json similarity index 95% rename from admin/i18n/pl/translations.json rename to admin/i18n/pl.json index 401a15b..8da40e8 100644 --- a/admin/i18n/pl/translations.json +++ b/admin/i18n/pl.json @@ -1,20 +1,20 @@ { - "Run as": "Uruchom jako", - "IP": "IP", - "Port": "Port", - "Secure(HTTPS)": "Bezpieczne (HTTPS)", "Authentication": "Uwierzytelnianie", - "Listen on all IPs": "Posłuchaj na wszystkich IP", - "help_tip": "Po zapisaniu adapter natychmiast uruchamia się ponownie z nową konfiguracją", - "Public certificate": "Certyfikat publiczny", - "Private certificate": "Prywatny certyfikat", "Chained certificate": "Przykuty certyfikat", + "IP": "IP", + "Language for only this instance": "Język tylko dla tej instancji", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Zakodujmy ustawienia", - "Use Lets Encrypt certificates": "Użyj Let's Encrypt certificates", - "Use this instance for automatic update": "Użyj tej instancji do automatycznej aktualizacji", + "Listen on all IPs": "Posłuchaj na wszystkich IP", + "Port": "Port", "Port to check the domain": "Port do sprawdzenia domeny", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Prywatny certyfikat", + "Public certificate": "Certyfikat publiczny", "Read about Let's Encrypt certificates": "Przeczytaj o certyfikatach Let's Encrypt", + "Run as": "Uruchom jako", + "Secure(HTTPS)": "Bezpieczne (HTTPS)", "Undo": "Cofnij", - "Language for only this instance": "Język tylko dla tej instancji" + "Use Lets Encrypt certificates": "Użyj Let's Encrypt certificates", + "Use this instance for automatic update": "Użyj tej instancji do automatycznej aktualizacji", + "help_tip": "Po zapisaniu adapter natychmiast uruchamia się ponownie z nową konfiguracją" } \ No newline at end of file diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt.json similarity index 96% rename from admin/i18n/pt/translations.json rename to admin/i18n/pt.json index a8fa51a..bf41e45 100644 --- a/admin/i18n/pt/translations.json +++ b/admin/i18n/pt.json @@ -1,20 +1,20 @@ { - "Run as": "Correr como", - "IP": "IP", - "Port": "Porta", - "Secure(HTTPS)": "Seguro (HTTPS)", "Authentication": "Autenticação", - "Listen on all IPs": "Ouça todos os IPs", - "help_tip": "Em salvar, o adaptador reinicia com a nova configuração imediatamente", - "Public certificate": "Certificado público", - "Private certificate": "Certificado privado", "Chained certificate": "Certificado acorrentado", + "IP": "IP", + "Language for only this instance": "Idioma apenas para esta instância", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Vamos criptografar configurações", - "Use Lets Encrypt certificates": "Use Vamos criptografar certificados", - "Use this instance for automatic update": "Use esta instância para atualização automática", + "Listen on all IPs": "Ouça todos os IPs", + "Port": "Porta", "Port to check the domain": "Porta para verificar o domínio", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "Certificado privado", + "Public certificate": "Certificado público", "Read about Let's Encrypt certificates": "Leia sobre os certificados Let's Encrypt", + "Run as": "Correr como", + "Secure(HTTPS)": "Seguro (HTTPS)", "Undo": "Desfazer", - "Language for only this instance": "Idioma apenas para esta instância" + "Use Lets Encrypt certificates": "Use Vamos criptografar certificados", + "Use this instance for automatic update": "Use esta instância para atualização automática", + "help_tip": "Em salvar, o adaptador reinicia com a nova configuração imediatamente" } \ No newline at end of file diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru.json similarity index 94% rename from admin/i18n/ru/translations.json rename to admin/i18n/ru.json index 3a0065b..edc5b43 100644 --- a/admin/i18n/ru/translations.json +++ b/admin/i18n/ru.json @@ -1,20 +1,20 @@ { - "Run as": "Запустить от пользователя", - "IP": "IP", - "Port": "Порт", - "Secure(HTTPS)": "Шифрование(HTTPS)", "Authentication": "Аутентификация", - "Listen on all IPs": "Открыть сокет на всех IP адресах", - "help_tip": "Сразу после сохранения настроек драйвер перезапуститься с новыми значениями", - "Public certificate": "'Public' сертификат", - "Private certificate": "'Private' сертификат", "Chained certificate": "'Chained' сертификат", + "IP": "IP", + "Language for only this instance": "Язык только для этого экземпляра", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "Настройки Let's Encrypt", - "Use Lets Encrypt certificates": "Использовать сертификаты Let's Encrypt", - "Use this instance for automatic update": "Обновлять сертификаты в этом драйвере", + "Listen on all IPs": "Открыть сокет на всех IP адресах", + "Port": "Порт", "Port to check the domain": "Порт для проверки доменного имени", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "'Private' сертификат", + "Public certificate": "'Public' сертификат", "Read about Let's Encrypt certificates": "Читать о сертификатах Let's Encrypt", + "Run as": "Запустить от пользователя", + "Secure(HTTPS)": "Шифрование(HTTPS)", "Undo": "Отменить", - "Language for only this instance": "Язык только для этого экземпляра" + "Use Lets Encrypt certificates": "Использовать сертификаты Let's Encrypt", + "Use this instance for automatic update": "Обновлять сертификаты в этом драйвере", + "help_tip": "Сразу после сохранения настроек драйвер перезапуститься с новыми значениями" } \ No newline at end of file diff --git a/admin/i18n/uk/translations.json b/admin/i18n/uk.json similarity index 99% rename from admin/i18n/uk/translations.json rename to admin/i18n/uk.json index 1cadba4..bc13473 100644 --- a/admin/i18n/uk/translations.json +++ b/admin/i18n/uk.json @@ -17,4 +17,4 @@ "Use Lets Encrypt certificates": "Використовуйте сертифікати Let's Encrypt", "Use this instance for automatic update": "Використовуйте цей екземпляр для автоматичного оновлення", "help_tip": "Після збереження адаптер негайно перезавантажується з новою конфігурацією" -} +} \ No newline at end of file diff --git a/admin/i18n/zh-cn/translations.json b/admin/i18n/zh-cn.json similarity index 92% rename from admin/i18n/zh-cn/translations.json rename to admin/i18n/zh-cn.json index cd78b50..b2e891f 100644 --- a/admin/i18n/zh-cn/translations.json +++ b/admin/i18n/zh-cn.json @@ -1,20 +1,20 @@ { - "Run as": "运行方式", - "IP": "知识产权", - "Port": "港口", - "Secure(HTTPS)": "安全(HTTPS)", "Authentication": "验证", - "Listen on all IPs": "监听所有 IP", - "help_tip": "保存后适配器立即使用新配置重新启动", - "Public certificate": "公共证书", - "Private certificate": "私人证书", "Chained certificate": "链式证书", + "IP": "知识产权", + "Language for only this instance": "仅此实例的语言", + "Let's Encrypt SSL": "Let's Encrypt SSL", "Let's Encrypt settings": "让我们加密设置", - "Use Lets Encrypt certificates": "使用 Let's Encrypt 证书", - "Use this instance for automatic update": "使用此实例进行自动更新", + "Listen on all IPs": "监听所有 IP", + "Port": "港口", "Port to check the domain": "检查域的端口", - "Let's Encrypt SSL": "Let's Encrypt SSL", + "Private certificate": "私人证书", + "Public certificate": "公共证书", "Read about Let's Encrypt certificates": "阅读 Let's Encrypt 证书", + "Run as": "运行方式", + "Secure(HTTPS)": "安全(HTTPS)", "Undo": "撤消", - "Language for only this instance": "仅此实例的语言" + "Use Lets Encrypt certificates": "使用 Let's Encrypt 证书", + "Use this instance for automatic update": "使用此实例进行自动更新", + "help_tip": "保存后适配器立即使用新配置重新启动" } \ No newline at end of file diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json index 01c1bba..27cf785 100644 --- a/admin/jsonConfig.json +++ b/admin/jsonConfig.json @@ -1,132 +1,130 @@ { - "type": "tabs", - "i18n": true, - "items": { - "mainTab": { - "type": "panel", - "label": "Main settings", - "items": { - "bind": { - "type": "ip", - "listenOnAllPorts": true, - "label": "IP", - "sm": 12, - "md": 8, - "lg": 5 + "type": "tabs", + "i18n": true, + "items": { + "mainTab": { + "type": "panel", + "label": "Main settings", + "items": { + "bind": { + "type": "ip", + "listenOnAllPorts": true, + "label": "IP", + "sm": 12, + "md": 8, + "lg": 5 + }, + "port": { + "type": "number", + "min": 1, + "max": 65565, + "label": "Port", + "sm": 12, + "md": 4, + "lg": 3 + }, + "secure": { + "newLine": true, + "type": "checkbox", + "label": "Secure(HTTPS)", + "sm": 12, + "md": 6, + "lg": 2 + }, + "certPublic": { + "type": "certificate", + "hidden": "!data.secure", + "certType": "public", + "validator": "!data.secure || data.certPublic", + "label": "Public certificate", + "sm": 12, + "md": 6, + "lg": 2 + }, + "certPrivate": { + "hidden": "!data.secure", + "type": "certificate", + "certType": "private", + "validator": "!data.secure || data.certPrivate", + "label": "Private certificate", + "sm": 12, + "md": 6, + "lg": 2 + }, + "certChained": { + "hidden": "!data.secure", + "type": "certificate", + "certType": "chained", + "label": "Chained certificate", + "sm": 12, + "md": 6, + "lg": 2 + }, + "auth": { + "newLine": true, + "type": "checkbox", + "confirm": { + "condition": "!data.secure && data.auth", + "title": "Warning!", + "text": "Unsecure_Auth", + "ok": "Ignore warning", + "cancel": "Disable authentication", + "type": "warning", + "alsoDependsOn": ["secure"] + }, + "label": "Authentication", + "sm": 12, + "md": 6, + "lg": 2 + }, + "defaultUser": { + "hidden": "!!data.auth", + "type": "user", + "label": "Run as", + "sm": 12, + "md": 6, + "lg": 2 + }, + "ttl": { + "hidden": "!data.auth", + "type": "number", + "label": "Login timeout", + "help": "sec", + "sm": 12, + "md": 6, + "lg": 2 + }, + "language": { + "newLine": true, + "label": "Language for only this instance", + "system": true, + "type": "language", + "sm": 12, + "md": 4, + "lg": 2 + } + } }, - "port": { - "type": "number", - "min": 1, - "max": 65565, - "label": "Port", - "sm": 12, - "md": 4, - "lg": 3 - }, - "secure": { - "newLine": true, - "type": "checkbox", - "label": "Secure(HTTPS)", - "sm": 12, - "md": 6, - "lg": 2 - }, - "certPublic": { - "type": "certificate", - "hidden": "!data.secure", - "certType": "public", - "validator": "!data.secure || data.certPublic", - "label": "Public certificate", - "sm": 12, - "md": 6, - "lg": 2 - }, - "certPrivate": { - "hidden": "!data.secure", - "type": "certificate", - "certType": "private", - "validator": "!data.secure || data.certPrivate", - "label": "Private certificate", - "sm": 12, - "md": 6, - "lg": 2 - }, - "certChained": { - "hidden": "!data.secure", - "type": "certificate", - "certType": "chained", - "label": "Chained certificate", - "sm": 12, - "md": 6, - "lg": 2 - }, - "auth": { - "newLine": true, - "type": "checkbox", - "confirm": { - "condition": "!data.secure && data.auth", - "title": "Warning!", - "text": "Unsecure_Auth", - "ok": "Ignore warning", - "cancel": "Disable authentication", - "type": "warning", - "alsoDependsOn": [ - "secure" - ] - }, - "label": "Authentication", - "sm": 12, - "md": 6, - "lg": 2 - }, - "defaultUser": { - "hidden": "!!data.auth", - "type": "user", - "label": "Run as", - "sm": 12, - "md": 6, - "lg": 2 - }, - "ttl": { - "hidden": "!data.auth", - "type": "number", - "label": "Login timeout", - "help": "sec", - "sm": 12, - "md": 6, - "lg": 2 - }, - "language": { - "newLine": true, - "label": "Language for only this instance", - "system": true, - "type": "language", - "sm": 12, - "md": 4, - "lg": 2 - } - } - }, - "leTab": { - "type": "panel", - "label": "Let's Encrypt SSL", - "disabled": "!data.secure", - "items": { - "_image": { - "type": "staticImage", - "tooltip": "Read about Let's Encrypt certificates", - "href": "https://github.com/ioBroker/ioBroker.admin/blob/master/README.md#lets-encrypt-certificates", - "src": "../../img/le.png", - "style": { - "width": 200, - "height": 59 - } - }, - "_staticText": { - "type": "staticText", - "text": "ra_Use iobroker.acme adapter for letsencrypt certificates" + "leTab": { + "type": "panel", + "label": "Let's Encrypt SSL", + "disabled": "!data.secure", + "items": { + "_image": { + "type": "staticImage", + "tooltip": "Read about Let's Encrypt certificates", + "href": "https://github.com/ioBroker/ioBroker.admin/blob/master/README.md#lets-encrypt-certificates", + "src": "../../img/le.png", + "style": { + "width": 200, + "height": 59 + } + }, + "_staticText": { + "type": "staticText", + "text": "ra_Use iobroker.acme adapter for letsencrypt certificates" + } + } } - } } - } -} \ No newline at end of file +} diff --git a/admin/ws.png b/admin/ws.png deleted file mode 100644 index b1ae4b79b13865b69a7c225696eb03d4becdb881..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1423 zcmV;A1#tR_P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;Eegmrwuz1prAzK~#8N?Og3OEI|-m2_OPQfCvzQ-3Sl? zB0vO)01+U=eecezrnlx+PtRUj|g#294Fon*-6vl}U7oQsQ#YKGAmicxQGZ|TPxGETKX7y#lE8>^$JrGaG}xKzt$ zB*Z~0HYIa6E5|{{qS8DpBdxu=SqIEbGLC~~B%~{%G7bo2tU*vzHV&4No}Zt`&&i5A zt(|ia1F_QO#~`4~<}^AUS*~?|f8UgGuzQZ3+|$z&om;GtJFo_j9gDaLEEbDyTvHNL z?Gz;kO9}SCLi*TL{e8*Vij!TF13~e0+SYwCgD1Rovd*#w!;DJ9vsH3D_!vEkyha zR^zE1dsrY%eK2ORh?Z-$68Qc7UBy7wAv0HHQ1=6h=pP;)9(oG3f^(?oB`V3m+<3*! z&d$zy3N_pded=*I=Bzm}H-k+?+=hFFs(>Ym?yO%CMfhN5uz2HB)UqPjuNIVwKcR}B zBTA>Do)v7n@5&%7K$j@Mhk~(V$=gwOfQ4>P3JPgYLiE$u2K1djdpt zc`iAIu4Eibxt4>uld&y6r-53<>+5UJ4qF2$Xgh2Tq@eAvHIRa~LuVjX<|uv4h}rVN zoEfzlgKFhLK6L+#qi!80fbC*eS6B57WL<6hEV1RgTN0mLq6la~siVe_YoRb->Y|e6 zXFs3GXaU#>GK2N&xbCgRSgJ0mpi+kn^h9DshxPe>p_mY-j~gL`lIeC&8Lujj-{E=oiPw$33OgZid@X-ACzQ ztiehz2L}iJF)=p-MVtiMwQ2-o4d(9j;th>wm$B?!1yEAhs``d|uk z6_t2=ety!%z*?WhtP3_5aMFk50J}2-Q*8aE!M1d8v}jR<2|bZyl3Fyt1-#{2p=;x-l!oXSG#h0k@E-<+8z+B*u{C9|xoY z+-@j{=O7HzggeCoZ_zr-r6l^un%9&7Ruy7Gij{>nV3f%x@J#?#7u*3x16h#A@0=XE db>hlw_6LCS(a3rnlhObH002ovPDHLkV1i-Nk+J{) diff --git a/admin/ws.svg b/admin/ws.svg new file mode 100644 index 0000000..3a447c0 --- /dev/null +++ b/admin/ws.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/lib/socket.io.js b/dist/lib/socket.io.js similarity index 100% rename from lib/socket.io.js rename to dist/lib/socket.io.js diff --git a/dist/lib/socketWS.d.ts b/dist/lib/socketWS.d.ts new file mode 100644 index 0000000..dd57444 --- /dev/null +++ b/dist/lib/socketWS.d.ts @@ -0,0 +1,22 @@ +import { SocketCommon, type Store } from '@iobroker/socket-classes'; +import type { Socket as WebSocketClient } from '@iobroker/ws-server'; +import type { AddressInfo } from 'node:net'; +import type { SocketSubscribeTypes } from '@iobroker/socket-classes/dist/types'; +export declare class SocketWS extends SocketCommon { + #private; + __getIsNoDisconnect(): boolean; + __initAuthentication(authOptions: { + store: Store; + secret: string; + checkUser?: (user: string, pass: string, cb: (error: Error | null, result?: { + logged_in: boolean; + }) => void) => void; + }): void; + __getUserFromSocket(socket: WebSocketClient, callback: (error: string | null, user?: string) => void): void; + __getClientAddress(socket: WebSocketClient): AddressInfo; + __updateSession(socket: WebSocketClient): boolean; + __getSessionID(socket: WebSocketClient): string | null; + publishAll(type: SocketSubscribeTypes, id: string, obj: ioBroker.Object | ioBroker.State | null | undefined): void; + publishFileAll(id: string, fileName: string, size: number | null): void; + publishInstanceMessageAll(sourceInstance: string, messageType: string, sid: string, data: any): void; +} diff --git a/dist/lib/socketWS.js b/dist/lib/socketWS.js new file mode 100644 index 0000000..af1bddb --- /dev/null +++ b/dist/lib/socketWS.js @@ -0,0 +1,220 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SocketWS = void 0; +const socket_classes_1 = require("@iobroker/socket-classes"); +const passport_1 = __importDefault(require("passport")); +const cookie_parser_1 = __importDefault(require("cookie-parser")); +// From settings used only secure, auth and crossDomain +class SocketWS extends socket_classes_1.SocketCommon { + __getIsNoDisconnect() { + return true; + } + #onAuthorizeSuccess = (data, accept) => { + this.adapter.log.debug(`successful connection to socket.io from ${(data.socket || data.connection).remoteAddress}`); + accept(false); + }; + #onAuthorizeFail = (data, message, critical, accept) => { + setTimeout(() => data.socket.emit(socket_classes_1.SocketCommon.COMMAND_RE_AUTHENTICATE), 100); + if (critical) { + this.adapter?.log.info(`failed connection to socket.io from ${(data.socket || data.connection).remoteAddress}: ${message}`); + } + // this error will be sent to the user as a special error-package + // see: http://socket.io/docs/client-api/#socket > error-object + if (critical) { + // @ts-expect-error + accept(new Error(message)); + } + else { + // @ts-expect-error + accept(new Error(`failed connection to socket.io: ${message}`)); //null, false); + } + }; + __initAuthentication(authOptions) { + if (authOptions.store && !this.store) { + this.store = authOptions.store; + } + else if (!authOptions.store && this.store) { + authOptions.store = this.store; + } + this.server?.use((0, socket_classes_1.passportSocket)({ + passport: passport_1.default, + cookieParser: cookie_parser_1.default, + checkUser: authOptions.checkUser, + secret: authOptions.secret, // the session_secret to parse the cookie + store: authOptions.store, // we NEED to use a sessionstore. no memorystore, please + success: this.#onAuthorizeSuccess, // *optional* callback on success - read more below + fail: this.#onAuthorizeFail, // *optional* callback on fail/error - read more below + })); + } + // Extract username from socket + __getUserFromSocket(socket, callback) { + let wait = false; + if (typeof callback !== 'function') { + return; + } + const user = socket.query.user; + const pass = socket.query.pass; + if (user && typeof user === 'string' && pass && typeof pass === 'string') { + wait = true; + void this.adapter.checkPassword(user, pass, res => { + if (res) { + this.adapter.log.debug(`Logged in: ${user}`); + if (typeof callback === 'function') { + callback(null, user); + } + else { + this.adapter.log.warn('[_getUserFromSocket] Invalid callback'); + } + } + else { + this.adapter.log.warn(`Invalid password or user name: ${user}, ${pass[0]}***(${pass.length})`); + if (typeof callback === 'function') { + callback('unknown user'); + } + else { + this.adapter.log.warn('[_getUserFromSocket] Invalid callback'); + } + } + }); + } + else { + try { + if (socket.conn.request.sessionID) { + socket._sessionID = socket.conn.request.sessionID; + if (this.store) { + wait = true; + this.store.get(socket.conn.request.sessionID, (_err, obj) => { + if (obj?.passport?.user) { + callback(null, obj.passport.user ? `system.user.${obj.passport.user}` : ''); + } + }); + } + } + } + catch { + // ignore + } + } + !wait && callback('Cannot detect user'); + } + __getClientAddress(socket) { + let address; + if (socket.connection) { + address = socket.connection && socket.connection.remoteAddress; + } + else { + // @ts-expect-error socket.io + address = socket.ws._socket.remoteAddress; + } + // @ts-expect-error socket.io + if (!address && socket.handshake) { + // @ts-expect-error socket.io + address = socket.handshake.address; + } + // @ts-expect-error socket.io + if (!address && socket.conn.request?.connection) { + // @ts-expect-error socket.io + address = socket.conn.request.connection.remoteAddress; + } + return address; + } + #waitForSessionEnd(socket) { + if (socket._sessionTimer) { + clearTimeout(socket._sessionTimer); + socket._sessionTimer = undefined; + } + const sessionId = socket._sessionID; + if (sessionId) { + this.store?.get(sessionId, (_err, obj) => { + if (obj) { + const expires = new Date(obj.cookie.expires); + const interval = expires.getTime() - Date.now(); + if (interval > 0) { + socket._sessionTimer ||= setTimeout(() => this.#waitForSessionEnd(socket), interval > 3600000 ? 3600000 : interval); + socket.emit('expire', expires.getTime()); + } + else { + this.adapter.log.warn('REAUTHENTICATE!'); + socket.emit(socket_classes_1.SocketCommon.COMMAND_RE_AUTHENTICATE); + } + } + else { + this.adapter.log.warn('REAUTHENTICATE!'); + socket?.emit?.(socket_classes_1.SocketCommon.COMMAND_RE_AUTHENTICATE); + } + }); + } + else { + socket?.emit?.(socket_classes_1.SocketCommon.COMMAND_RE_AUTHENTICATE); + } + } + // update session ID, but not ofter than 60 seconds + __updateSession(socket) { + const sessionId = socket._sessionID; + const now = Date.now(); + if (sessionId && (!socket._lastActivity || now - socket._lastActivity > 10000)) { + socket._lastActivity = now; + this.store?.get(sessionId, (_err, obj) => { + // obj = {"cookie":{"originalMaxAge":2592000000,"expires":"2020-09-24T18:09:50.377Z","httpOnly":true,"path":"/"},"passport":{"user":"admin"}} + if (obj) { + // start timer + if (!socket._sessionTimer) { + this.#waitForSessionEnd(socket); + } + /*obj.ttl = obj.ttl || (new Date(obj.cookie.expires).getTime() - now); + const expires = new Date(); + expires.setMilliseconds(expires.getMilliseconds() + obj.ttl + 10000); + obj.cookie.expires = expires.toISOString(); + console.log('Session ' + sessionId + ' expires on ' + obj.cookie.expires); + + this.store.set(sessionId, obj);*/ + } + else { + this.adapter.log.warn('REAUTHENTICATE!'); + socket.emit(socket_classes_1.SocketCommon.COMMAND_RE_AUTHENTICATE); + } + }); + } + return true; + } + __getSessionID(socket) { + return this.adapter.config.auth ? socket._sessionID || null : null; + } + publishAll(type, id, obj) { + if (id === undefined) { + console.log('Problem'); + } + this.server?.sockets?.connected.forEach(socket => this.publish(socket, type, id, obj)); + } + publishFileAll(id, fileName, size) { + if (id === undefined) { + console.log('Problem'); + } + if (this.server?.sockets) { + const sockets = this.server.sockets.sockets || this.server.sockets.connected; + for (const socket of sockets) { + if (this.publishFile(socket, id, fileName, size)) { + this.__updateSession(socket); + } + } + } + } + publishInstanceMessageAll(sourceInstance, messageType, sid, data) { + if (this.server?.sockets) { + const sockets = this.server.sockets.sockets || this.server.sockets.connected; + // this could be an object or array + for (const socket of sockets) { + if (socket.id === sid) { + if (this.publishInstanceMessage(socket, sourceInstance, messageType, data)) { + this.__updateSession(socket); + } + } + } + } + } +} +exports.SocketWS = SocketWS; +//# sourceMappingURL=socketWS.js.map \ No newline at end of file diff --git a/dist/lib/socketWS.js.map b/dist/lib/socketWS.js.map new file mode 100644 index 0000000..515fdac --- /dev/null +++ b/dist/lib/socketWS.js.map @@ -0,0 +1 @@ +{"version":3,"file":"socketWS.js","sourceRoot":"","sources":["../../src/lib/socketWS.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/main.d.ts b/dist/main.d.ts new file mode 100644 index 0000000..6035c25 --- /dev/null +++ b/dist/main.d.ts @@ -0,0 +1,18 @@ +import { Adapter, type AdapterOptions } from '@iobroker/adapter-core'; +export declare class WsAdapter extends Adapter { + private wsConfig; + private server; + private readonly socketIoFile; + private bruteForce; + private store; + private secret; + private certificates; + constructor(options?: Partial); + onUnload(callback: () => void): void; + onMessage(obj: ioBroker.Message): void; + checkUser(username: string, password: string, cb: (error: null | Error, result?: { + logged_in: boolean; + }) => void): void; + initWebServer(): void; + main(): Promise; +} diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 0000000..7b6a4b0 --- /dev/null +++ b/dist/main.js @@ -0,0 +1,300 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WsAdapter = void 0; +const node_crypto_1 = require("node:crypto"); +const session = __importStar(require("express-session")); +const adapter_core_1 = require("@iobroker/adapter-core"); // Get common adapter utils +const webserver_1 = require("@iobroker/webserver"); +const ws_server_1 = require("@iobroker/ws-server"); +const socketWS_1 = require("./lib/socketWS"); +const node_fs_1 = require("node:fs"); +class WsAdapter extends adapter_core_1.Adapter { + wsConfig; + server = { + server: null, + io: null, + app: null, + }; + socketIoFile; + bruteForce = {}; + store = null; + secret = 'Zgfr56gFe87jJOM'; + certificates; + constructor(options = {}) { + super({ + ...options, + name: 'ws', + unload: callback => this.onUnload(callback), + message: obj => this.onMessage(obj), + stateChange: (id, state) => { + this.server?.io?.publishAll('stateChange', id, state); + }, + ready: () => this.main(), + objectChange: (id, obj) => { + this.server?.io?.publishAll('objectChange', id, obj); + }, + fileChange: (id, fileName, size) => { + this.server?.io?.publishFileAll(id, fileName, size); + }, + }); + this.socketIoFile = (0, node_fs_1.readFileSync)(`${__dirname}/lib/socket.io.js`).toString('utf-8'); + this.wsConfig = this.config; + this.on('log', (obj) => this.server?.io?.sendLog(obj)); + } + onUnload(callback) { + try { + void this.setState('info.connected', '', true); + void this.setState('info.connection', false, true); + this.log.info(`terminating http${this.wsConfig.secure ? 's' : ''} server on port ${this.wsConfig.port}`); + this.server.io?.close(); + this.server.server?.close(); + callback(); + } + catch { + callback(); + } + } + onMessage(obj) { + if (obj?.command !== 'im') { + // if not instance message + return; + } + // to make messages shorter, we code the answer as: + // m - message type + // s - socket ID + // d - data + this.server?.io?.publishInstanceMessageAll(obj.from, obj.message.m, obj.message.s, obj.message.d); + } + checkUser(username, password, cb) { + username = (username || '') + .toString() + .replace(this.FORBIDDEN_CHARS, '_') + .replace(/\s/g, '_') + .replace(/\./g, '_') + .toLowerCase(); + if (this.bruteForce[username] && this.bruteForce[username].errors > 4) { + let minutes = Date.now() - this.bruteForce[username].time; + if (this.bruteForce[username].errors < 7) { + if (Date.now() - this.bruteForce[username].time < 60000) { + minutes = 1; + } + else { + minutes = 0; + } + } + else if (this.bruteForce[username].errors < 10) { + if (Date.now() - this.bruteForce[username].time < 180000) { + minutes = Math.ceil((180000 - minutes) / 60000); + } + else { + minutes = 0; + } + } + else if (this.bruteForce[username].errors < 15) { + if (Date.now() - this.bruteForce[username].time < 600000) { + minutes = Math.ceil((600000 - minutes) / 60000); + } + else { + minutes = 0; + } + } + else if (Date.now() - this.bruteForce[username].time < 3600000) { + minutes = Math.ceil((3600000 - minutes) / 60000); + } + else { + minutes = 0; + } + if (minutes) { + return cb(new Error(`Too many errors. Try again in ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}.`)); + } + } + void this.checkPassword(username, password, (success, _user) => { + if (!success) { + this.bruteForce[username] = this.bruteForce[username] || { errors: 0 }; + this.bruteForce[username].time = Date.now(); + this.bruteForce[username].errors++; + } + else if (this.bruteForce[username]) { + delete this.bruteForce[username]; + } + if (success) { + return cb(null, { logged_in: true }); + } + return cb(null); + }); + } + initWebServer() { + this.wsConfig.port = parseInt(this.wsConfig.port, 10) || 0; + if (this.wsConfig.port) { + if (this.wsConfig.secure && !this.certificates) { + return; + } + this.wsConfig.ttl = this.wsConfig.ttl || 3600; + if (this.wsConfig.auth) { + const AdapterStore = adapter_core_1.commonTools.session(session, this.wsConfig.ttl); + // Authentication checked by server itself + this.store = new AdapterStore({ adapter: this }); + } + this.getPort(this.wsConfig.port, !this.wsConfig.bind || this.wsConfig.bind === '0.0.0.0' ? undefined : this.wsConfig.bind || undefined, async (port) => { + if (parseInt(port, 10) !== this.wsConfig.port) { + this.log.error(`port ${this.wsConfig.port} already in use`); + return this.terminate + ? this.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + this.server.app = (req, res) => { + if (req.url?.includes('socket.io.js')) { + // @ts-expect-error + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(this.socketIoFile); + } + else { + // @ts-expect-error + res.writeHead(404); + res.end('Not found'); + } + }; + try { + const webserver = new webserver_1.WebServer({ + adapter: this, + secure: this.wsConfig.secure, + app: this.server.app, + }); + this.server.server = await webserver.init(); + } + catch (err) { + this.log.error(`Cannot create server: ${err}`); + this.terminate + ? this.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + return; + } + if (!this.server.server) { + this.log.error(`Cannot create server`); + this.terminate + ? this.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + return; + } + let serverListening = false; + this.server.server.on('error', e => { + if (e.toString().includes('EACCES') && port <= 1024) { + this.log.error(`node.js process has no rights to start server on the port ${port}.\n` + + 'Do you know that on linux you need special permissions for ports under 1024?\n' + + 'You can call in shell following scrip to allow it for node.js: "iobroker fix"'); + } + else { + this.log.error(`Cannot start server on ${this.wsConfig.bind || '0.0.0.0'}:${port}: ${e}`); + } + if (!serverListening) { + this.terminate + ? this.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + }); + // Start the web server + this.server.server.listen(this.wsConfig.port, !this.wsConfig.bind || this.wsConfig.bind === '0.0.0.0' + ? undefined + : this.wsConfig.bind || undefined, () => { + void this.setState('info.connection', true, true); + serverListening = true; + }); + const settings = { + ttl: this.wsConfig.ttl, + port: this.wsConfig.port, + secure: this.wsConfig.secure, + auth: this.wsConfig.auth, + crossDomain: true, + forceWebSockets: true, // this is irrelevant for ws + defaultUser: this.wsConfig.defaultUser, + }; + this.server.io = new socketWS_1.SocketWS(settings, this); + this.server.io.start(this.server.server, ws_server_1.SocketIO, { + checkUser: this.checkUser, + store: this.store, + secret: this.secret, + }); + }); + } + else { + this.log.error('port missing'); + this.terminate + ? this.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + } + async main() { + this.wsConfig = this.config; + if (this.wsConfig.auth) { + // Generate secret for session manager + const systemConfig = await this.getForeignObjectAsync('system.config'); + if (systemConfig) { + if (!systemConfig.native?.secret) { + systemConfig.native = systemConfig.native || {}; + await new Promise(resolve => (0, node_crypto_1.randomBytes)(24, (_err, buf) => { + this.secret = buf.toString('hex'); + void this.extendForeignObject('system.config', { native: { secret: this.secret } }); + resolve(); + })); + } + else { + this.secret = systemConfig.native.secret; + } + } + else { + this.log.error('Cannot find object system.config'); + } + } + if (this.wsConfig.secure) { + // Load certificates + await new Promise(resolve => this.getCertificates(undefined, undefined, undefined, (_err, certificates) => { + this.certificates = certificates; + resolve(); + })); + } + this.initWebServer(); + } +} +exports.WsAdapter = WsAdapter; +if (require.main !== module) { + // Export the constructor in compact mode + module.exports = (options) => new WsAdapter(options); +} +else { + // otherwise start the instance directly + (() => new WsAdapter())(); +} +//# sourceMappingURL=main.js.map \ No newline at end of file diff --git a/dist/main.js.map b/dist/main.js.map new file mode 100644 index 0000000..7459a7a --- /dev/null +++ b/dist/main.js.map @@ -0,0 +1 @@ +{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7093886 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,26 @@ +import config from '@iobroker/eslint-config'; + +export default [ + ...config, + { + languageOptions: { + parserOptions: { + allowDefaultProject: { + allow: ['*.js', '*.mjs'], + }, + tsconfigRootDir: import.meta.dirname, + project: './tsconfig.json', + }, + }, + }, + { + ignores: ['dist/*', 'example/*', 'test/*', 'eslint.config.mjs', 'prettier.config.mjs', 'tasks.js'], + }, + { + // disable temporary the rule 'jsdoc/require-param' and enable 'jsdoc/require-jsdoc' + rules: { + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + }, + }, +]; diff --git a/example/conn.js b/example/conn.js index f74ba49..1b3fe56 100644 --- a/example/conn.js +++ b/example/conn.js @@ -1,3 +1,4 @@ +// Deprecated. Use socket-client/Connection ////// ----------------------- Connection "class" ---------------------- //////////// /* jshint browser: true */ /* global document */ @@ -1264,7 +1265,7 @@ var servConn = { if (!this._authRunning) { this._authRunning = true; - // Try to read version + // Try to read a version this._checkAuth(function (version) { // If we have got version string, so there is no authentication, or we are authenticated that._authRunning = false; diff --git a/example/index.html b/example/index.html index 0343f5a..e8f979a 100644 --- a/example/index.html +++ b/example/index.html @@ -1,53 +1,52 @@ - - - - - - - - - - + + +
+ - + const socket = new Connection({ + name: 'example.0', + port: 8094, + host: 'localhost', + protocol: 'http', + onProgress: progress => { + if (progress === 0) { + printLine('disconnected'); + } + }, + onReady: () => { + printLine('connected'); + socket.getStates('system.adapter.admin.*').then(_states => { + let count = 0; + for (let id in _states) { + count++; + } + printLine(`Received ${count} states.`); + states = _states; + }); + + socket.subscribeState('system.adapter.admin.0.*', (id, state) => { + printLine(`NEW VALUE of ${id}: ${JSON.stringify(state)}`); + }); + }, + onError: err => { + debugger; + printLine(`Cannot execute %s for %s, because of insufficient permissions: ${err}`); + }, + }); + + + diff --git a/example/socket-client/Connection.d.ts b/example/socket-client/Connection.d.ts new file mode 100644 index 0000000..5c40b4f --- /dev/null +++ b/example/socket-client/Connection.d.ts @@ -0,0 +1,676 @@ +import type { ConnectionProps, LogMessage } from './ConnectionProps.js'; +import type { EmitEventHandler, ListenEventHandler, SocketClient } from './SocketClient.js'; +/** Possible progress states. */ +export declare enum PROGRESS { + /** The socket is connecting. */ + CONNECTING = 0, + /** The socket is successfully connected. */ + CONNECTED = 1, + /** All objects are loaded. */ + OBJECTS_LOADED = 2, + /** The socket is ready for use. */ + READY = 3 +} +export declare enum ERRORS { + PERMISSION_ERROR = "permissionError", + NOT_CONNECTED = "notConnectedError", + TIMEOUT = "timeout", + NOT_ADMIN = "Allowed only in admin", + NOT_SUPPORTED = "Not supported" +} +/** @deprecated Use {@link ERRORS.PERMISSION_ERROR} instead */ +export declare const PERMISSION_ERROR = ERRORS.PERMISSION_ERROR; +/** @deprecated Use {@link ERRORS.NOT_CONNECTED} instead */ +export declare const NOT_CONNECTED = ERRORS.NOT_CONNECTED; +/** + * @internal + */ +export interface RequestOptions { + /** The key that is used to cache the results for later requests of the same kind */ + cacheKey?: string; + /** Used to bypass the cache */ + forceUpdate?: boolean; + /** Can be used to identify the request method in error messages */ + requestName?: string; + /** + * The timeout in milliseconds after which the call will reject with a timeout error. + * If no timeout is given, the default is used. Set this to `false` to explicitly disable the timeout. + */ + commandTimeout?: number | false; + /** Will be called when the timeout elapses */ + onTimeout?: () => void; + /** Whether the call should only be allowed in the admin adapter */ + requireAdmin?: boolean; + /** Require certain features to be supported for this call */ + requireFeatures?: string[]; + /** The function that does the actual work */ + executor: (resolve: (value: T | PromiseLike | Promise) => void, reject: (reason?: any) => void, + /** Can be used to check in the executor whether the request has timed out and/or stop it from timing out */ + timeout: Readonly<{ + elapsed: boolean; + clearTimeout: () => void; + }>) => void | Promise; +} +export type BinaryStateChangeHandler = (id: string, base64: string | null) => void; +export type FileChangeHandler = (id: string, fileName: string, size: number | null) => void; +export interface OldObject { + _id: string; + type: string; +} +export type ObjectChangeHandler = (id: string, obj: ioBroker.Object | null | undefined, oldObj?: OldObject) => void | Promise; +export type InstanceMessageCallback = (data: any, sourceInstance: string, messageType: string) => void | Promise; +export type InstanceSubscribe = { + messageType: string; + callback: InstanceMessageCallback; +}; +export declare class Connection = Record, CustomEmitEvents extends Record = Record> { + constructor(props: Partial); + private applyDefaultProps; + private readonly props; + private ignoreState; + private connected; + private subscribed; + private firstConnect; + waitForRestart: boolean; + loaded: boolean; + private simStates; + private readonly statesSubscribes; + private readonly filesSubscribes; + private readonly objectsSubscribes; + private objects; + private states; + acl: any; + isSecure: boolean; + onReadyDone: boolean; + private readonly onConnectionHandlers; + private readonly onLogHandlers; + private onCmdStdoutHandler?; + private onCmdStderrHandler?; + private onCmdExitHandler?; + private onError; + /** The socket instance */ + protected _socket: SocketClient; + private _waitForSocketPromise?; + private readonly _waitForFirstConnectionPromise; + /** array with all subscriptions to instances */ + private _instanceSubscriptions; + /** Cache for server requests */ + private readonly _promises; + protected _authTimer: any; + protected _systemConfig?: ioBroker.SystemConfigObject; + /** The "system.config" object */ + get systemConfig(): Readonly | undefined; + /** System language. It could be changed during runtime */ + systemLang: ioBroker.Languages; + /** + * Checks if this connection is running in a web adapter and not in an admin. + * + * @returns True if running in a web adapter or in a socketio adapter. + */ + static isWeb(): boolean; + private waitForSocketLib; + /** + * Starts the socket.io connection. + */ + startSocket(): Promise; + /** + * Called internally. + */ + private onPreConnect; + /** + * Checks if running in ioBroker cloud + */ + static isCloud(): boolean; + /** + * Checks if the socket is connected. + * + * @returns true if connected. + */ + isConnected(): boolean; + /** + * Returns a promise which is resolved when the socket is connected. + */ + waitForFirstConnection(): Promise; + /** + * Called internally. + */ + private getUserPermissions; + /** Loads the important data and retries a couple of times if it takes too long */ + private loadData; + /** + * Called after the socket is connected. Loads the necessary data. + */ + private doLoadData; + /** + * Called internally. + */ + private authenticate; + /** + * Subscribe to the changes of the given state. + * In compare to the subscribeObject method, + * this method calls the handler with the current state value immediately after subscribing. + * + * @param id The ioBroker state ID or array of state IDs. + * @param binary Set to true if the given state is binary and requires Base64 decoding. + * @param cb The callback. + */ + subscribeState(id: string | string[], binary: true, cb: BinaryStateChangeHandler): Promise; + subscribeState(id: string | string[], binary: false, cb: ioBroker.StateChangeHandler): Promise; + subscribeState(id: string | string[], cb: ioBroker.StateChangeHandler): Promise; + /** + * Subscribe to the changes of the given state and wait for answer. + * + * @param id The ioBroker state ID. + * @param cb The callback. + */ + subscribeStateAsync(id: string | string[], cb: ioBroker.StateChangeHandler): Promise; + /** + * Unsubscribes the given callback from changes of the given state. + * + * @param id The ioBroker state ID or array of state IDs. + * @param cb The callback. + */ + unsubscribeState(id: string | string[], cb?: ioBroker.StateChangeHandler): void; + /** + * Subscribe to changes of the given object. + * In compare to the subscribeState method, + * this method does not call the handler with the current value immediately after subscribe. + * + * the current value. + * + * @param id The ioBroker object ID. + * @param cb The callback. + */ + subscribeObject(id: string | string[], cb: ObjectChangeHandler): Promise; + /** + * Unsubscribes all callbacks from changes of the given object. + * + * @param id The ioBroker object ID. + */ + /** + * Unsubscribes the given callback from changes of the given object. + * + * @param id The ioBroker object ID. + * @param cb The callback. + */ + unsubscribeObject(id: string | string[], cb?: ObjectChangeHandler): Promise; + /** + * Called internally. + * + * @param id The ioBroker object ID. + * @param obj The new object. + */ + private objectChange; + /** + * Called internally. + * + * @param id The ioBroker state ID. + * @param state The new state value. + */ + private stateChange; + /** + * Called internally. + * + * @param messageType The message type from the instance + * @param sourceInstance The source instance + * @param data The message data + */ + private instanceMessage; + /** + * Called internally. + * + * @param id The ioBroker object ID of type 'meta'. + * @param fileName - file name + * @param size - size of the file + */ + private fileChange; + /** + * Subscribe to changes of the files. + * + * @param id The ioBroker state ID for a "meta" object. Could be a pattern + * @param filePattern Pattern or file name, like 'main/*' or 'main/visViews.json` + * @param cb The callback. + */ + subscribeFiles(id: string, filePattern: string | string[], cb: FileChangeHandler): Promise; + /** + * Unsubscribes the given callback from changes of files. + * + * @param id The ioBroker state ID. + * @param filePattern Pattern or file name, like 'main/*' or 'main/visViews.json` + * @param cb The callback. + */ + unsubscribeFiles(id: string, filePattern: string | string[], cb?: FileChangeHandler): void; + /** Requests data from the server or reads it from the cache */ + protected request({ cacheKey, forceUpdate, commandTimeout, onTimeout, requireAdmin, requireFeatures, executor, }: RequestOptions): Promise; + /** + * Deletes cached promise. + * So next time the information will be requested anew + */ + resetCache(key: string, isAll?: boolean): void; + /** + * Gets all states. + * + * @param pattern Pattern of states or array of IDs + */ + getStates(pattern?: string | string[]): Promise>; + /** + * Gets the given state. + * + * @param id The state ID. + */ + getState(id: string): Promise; + /** + * Gets the given binary state Base64 encoded. + * + * @deprecated since js-controller 5.0. Use files instead. + * @param id The state ID. + */ + getBinaryState(id: string): Promise; + /** + * Sets the given binary state. + * + * @deprecated since js-controller 5.0. Use files instead. + * @param id The state ID. + * @param base64 The Base64 encoded binary data. + */ + setBinaryState(id: string, base64: string): Promise; + /** + * Sets the given state value. + * + * @param id The state ID. + * @param val The state value. + * @param ack Acknowledgement flag. + */ + setState(id: string, val: ioBroker.State | ioBroker.StateValue | ioBroker.SettableState, ack?: boolean): Promise; + /** + * Gets all objects. + * + * @param update Callback that is executed when all objects are retrieved. + */ + /** + * Gets all objects. + * + * @param update Set to true to retrieve all objects from the server (instead of using the local cache). + * @param disableProgressUpdate don't call onProgress() when done + */ + getObjects(update?: boolean, disableProgressUpdate?: boolean): Promise>; + /** + * Gets the list of objects by ID. + * + * @param list array of IDs to retrieve + */ + getObjectsById(list: string[]): Promise | undefined>; + /** + * Called internally. + * + * @param isEnable Set to true if subscribing, false to unsubscribe. + */ + private _subscribe; + /** + * Requests log updates. + * + * @param isEnabled Set to true to get logs. + */ + requireLog(isEnabled: boolean): Promise; + /** + * Deletes the given object. + * + * @param id The object ID. + * @param maintenance Force deletion of non conform IDs. + */ + delObject(id: string, maintenance?: boolean): Promise; + /** + * Deletes the given object and all its children. + * + * @param id The object ID. + * @param maintenance Force deletion of non conform IDs. + */ + delObjects(id: string, maintenance: boolean): Promise; + /** + * Sets the object. + * + * @param id The object ID. + * @param obj The object. + */ + setObject(id: string, obj: ioBroker.SettableObject): Promise; + /** + * Gets the object with the given id from the server. + * + * @param id The object ID. + * @returns The object. + */ + getObject(id: T): ioBroker.GetObjectPromise; + /** + * Sends a message to a specific instance or all instances of some specific adapter. + * + * @param instance The instance to send this message to. + * @param command Command name of the target instance. + * @param data The message data to send. + */ + sendTo(instance: string, command: string, data?: any): Promise; + /** + * Extend an object and create it if it might not exist. + * + * @param id The id. + * @param obj The object. + */ + extendObject(id: string, obj: ioBroker.PartialObject): Promise; + /** + * Register a handler for log messages. + * + * @param handler The handler. + */ + registerLogHandler(handler: (message: LogMessage) => void): void; + /** + * Unregister a handler for log messages. + * + * @param handler The handler. + */ + unregisterLogHandler(handler: (message: LogMessage) => void): void; + /** + * Register a handler for the connection state. + * + * @param handler The handler. + */ + registerConnectionHandler(handler: (connected: boolean) => void): void; + /** + * Unregister a handler for the connection state. + * + * @param handler The handler. + */ + unregisterConnectionHandler(handler: (connected: boolean) => void): void; + /** + * Set the handler for standard output of a command. + * + * @param handler The handler. + */ + registerCmdStdoutHandler(handler: (id: string, text: string) => void): void; + /** + * Unset the handler for standard output of a command. + */ + unregisterCmdStdoutHandler(): void; + /** + * Set the handler for standard error of a command. + * + * @param handler The handler. + */ + registerCmdStderrHandler(handler: (id: string, text: string) => void): void; + /** + * Unset the handler for standard error of a command. + */ + unregisterCmdStderrHandler(): void; + /** + * Set the handler for exit of a command. + * + * @param handler The handler. + */ + registerCmdExitHandler(handler: (id: string, exitCode: number) => void): void; + /** + * Unset the handler for exit of a command. + */ + unregisterCmdExitHandler(): void; + /** + * Get all enums with the given name. + * + * @param _enum The name of the enum, like `rooms` or `functions` + * @param update Force update. + */ + getEnums(_enum?: string, update?: boolean): Promise>; + /** + * @deprecated since version 1.1.15, cause parameter order does not match backend + * Query a predefined object view. + * @param start The start ID. + * @param end The end ID. + * @param type The type of object. + */ + getObjectView(start: string | undefined, end: string | undefined, type: T): Promise>; + /** + * Query a predefined object view. + * + * @param type The type of object. + * @param start The start ID. + * @param [end] The end ID. + */ + getObjectViewSystem(type: T, start?: string, end?: string): Promise>; + /** + * Query a predefined object view. + * + * @param design design - 'system' or other designs like `custom`. + * @param type The type of object. + * @param start The start ID. + * @param [end] The end ID. + */ + getObjectViewCustom(design: string, type: T, start?: string, end?: string): Promise>; + /** + * Read the meta items. + */ + readMetaItems(): Promise; + /** + * Read the directory of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param path The directory name. + */ + readDir(namespace: string | null, path: string): Promise; + /** + * Read a file of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param fileName The file name. + * @param base64 If it must be a base64 format + */ + readFile(namespace: string | null, fileName: string, base64?: boolean): Promise<{ + file: string; + mimeType: string; + }>; + /** + * Write a file of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param fileName The file name. + * @param data The data (if it's a Buffer, it will be converted to Base64). + */ + writeFile64(namespace: string, fileName: string, data: ArrayBuffer | string): Promise; + /** + * Delete a file of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param fileName The file name. + */ + deleteFile(namespace: string, fileName: string): Promise; + /** + * Delete a folder of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param folderName The folder name. + */ + deleteFolder(namespace: string, folderName: string): Promise; + /** + * Rename file or folder in ioBroker DB + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param oldName current file name, e.g., main/vis-views.json + * @param newName new file name, e.g., main/vis-views-new.json + */ + rename(namespace: string, oldName: string, newName: string): Promise; + /** + * Rename file in ioBroker DB + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param oldName current file name, e.g., main/vis-views.json + * @param newName new file name, e.g., main/vis-views-new.json + */ + renameFile(namespace: string, oldName: string, newName: string): Promise; + /** + * Execute a command on a host. + */ + cmdExec( + /** Host name */ + host: string, + /** Command to execute */ + cmd: string, + /** Command ID */ + cmdId: number, + /** Timeout of command in ms */ + cmdTimeout?: number): Promise; + /** + * Gets the system configuration. + * + * @param update Force update. + */ + getSystemConfig(update?: boolean): Promise; + getCompactSystemConfig(update?: boolean): Promise; + /** + * Read all states (which might not belong to this adapter) which match the given pattern. + * + * @param pattern The pattern to match. + */ + getForeignStates(pattern?: string | string[] | null): ioBroker.GetStatesPromise; + /** + * Get foreign objects by pattern, by specific type and resolve their enums. + * + * @param pattern The pattern to match. + * @param type The type of the object. + */ + getForeignObjects(pattern: string | null | undefined, type: T): Promise>; + /** + * Sets the system configuration. + * + * @param obj The new system configuration. + */ + setSystemConfig(obj: ioBroker.SystemConfigObject): Promise; + /** + * Get the raw socket.io socket. + */ + getRawSocket(): any; + /** + * Get the history of a given state. + * + * @param id The state ID. + * @param options The query options. + */ + getHistory(id: string, options: ioBroker.GetHistoryOptions): Promise; + /** + * Get the history of a given state. + * + * @param id The state ID. + * @param options The query options. + */ + getHistoryEx(id: string, options: ioBroker.GetHistoryOptions): Promise<{ + values: ioBroker.GetHistoryResult; + sessionId: number; + step: number; + }>; + /** + * Get the IP addresses of the given host. + * + * @param host The host name. + * @param update Force update. + */ + getIpAddresses(host: string, update?: boolean): Promise; + /** + * Gets the version. + */ + getVersion(update?: boolean): Promise<{ + version: string; + serverName: string; + }>; + /** + * Gets the web server name. + */ + getWebServerName(): Promise; + /** + * Check if the file exists + * + * @param adapter adapter name + * @param filename file name with the full path. it could be like vis.0/* + */ + fileExists(adapter: string, filename: string): Promise; + /** + * Read current user + */ + getCurrentUser(): Promise; + /** + * Get uuid + */ + getUuid(): Promise; + /** + * Checks if a given feature is supported. + * + * @param feature The feature to check. + * @param update Force update. + */ + checkFeatureSupported(feature: string, update?: boolean): Promise; + /** + * Get all adapter instances. + * + * @param update Force update. + */ + /** + * Get all instances of the given adapter. + * + * @param adapter The name of the adapter. + * @param update Force update. + */ + getAdapterInstances(adapter?: string | boolean, update?: boolean): Promise; + /** + * Get adapters with the given name. + * + * @param adapter The name of the adapter. + * @param update Force update. + */ + getAdapters(adapter?: string, update?: boolean): Promise; + /** + * Get the list of all groups. + * + * @param update Force update. + */ + getGroups(update?: boolean): Promise; + /** + * Logout current user + */ + logout(): Promise; + /** + * Subscribe on instance message + * + * @param targetInstance instance, like 'cameras.0' + * @param messageType message type like 'startCamera/cam3' + * @param data optional data object + * @param callback message handler + */ + subscribeOnInstance(targetInstance: string, messageType: string, data: any, callback: InstanceMessageCallback): Promise<{ + error?: string; + accepted?: boolean; + heartbeat?: number; + } | null>; + /** + * Unsubscribe from instance message + * + * @param targetInstance instance, like 'cameras.0' + * @param messageType message type like 'startCamera/cam3' + * @param callback message handler + */ + unsubscribeFromInstance(targetInstance: string, messageType: string, callback: InstanceMessageCallback): Promise; + /** + * Send log to ioBroker log + * + * @param text Log text + * @param level `info`, `debug`, `warn`, `error` or `silly` + */ + log(text: string, level?: string): Promise; + /** + * This is a special method for vis. + * It is used to not send to server the changes about "nothing_selected" state + * + * @param id The state that has to be ignored by communication + */ + setStateToIgnore(id: string): void; +} diff --git a/example/socket-client/Connection.js b/example/socket-client/Connection.js new file mode 100644 index 0000000..fe23607 --- /dev/null +++ b/example/socket-client/Connection.js @@ -0,0 +1,2290 @@ +import { createDeferredPromise } from './DeferredPromise.js'; +import { getObjectViewResultToArray, normalizeHostId, pattern2RegEx, wait } from './tools.js'; +/** Possible progress states. */ +export var PROGRESS; +(function (PROGRESS) { + /** The socket is connecting. */ + PROGRESS[PROGRESS["CONNECTING"] = 0] = "CONNECTING"; + /** The socket is successfully connected. */ + PROGRESS[PROGRESS["CONNECTED"] = 1] = "CONNECTED"; + /** All objects are loaded. */ + PROGRESS[PROGRESS["OBJECTS_LOADED"] = 2] = "OBJECTS_LOADED"; + /** The socket is ready for use. */ + PROGRESS[PROGRESS["READY"] = 3] = "READY"; +})(PROGRESS || (PROGRESS = {})); +export var ERRORS; +(function (ERRORS) { + ERRORS["PERMISSION_ERROR"] = "permissionError"; + ERRORS["NOT_CONNECTED"] = "notConnectedError"; + ERRORS["TIMEOUT"] = "timeout"; + ERRORS["NOT_ADMIN"] = "Allowed only in admin"; + ERRORS["NOT_SUPPORTED"] = "Not supported"; +})(ERRORS || (ERRORS = {})); +/** @deprecated Use {@link ERRORS.PERMISSION_ERROR} instead */ +export const PERMISSION_ERROR = ERRORS.PERMISSION_ERROR; +/** @deprecated Use {@link ERRORS.NOT_CONNECTED} instead */ +export const NOT_CONNECTED = ERRORS.NOT_CONNECTED; +const ADAPTERS = ['material', 'echarts', 'vis']; +export class Connection { + constructor(props) { + this.props = this.applyDefaultProps(props); + this.waitForSocketLib() + .then(() => this.startSocket()) + .catch(e => { + alert(`Socket connection could not be initialized: ${e}`); + }); + } + applyDefaultProps(props) { + return { + ...props, + // Define default props that always need to be set + protocol: props.protocol || window.location.protocol, + host: props.host || window.location.hostname, + port: props.port || (window.location.port === '3000' ? 8081 : window.location.port), + ioTimeout: Math.max(props.ioTimeout || 20000, 20000), + cmdTimeout: Math.max(props.cmdTimeout || 5000, 5000), + admin5only: props.admin5only || false, + autoSubscribes: props.autoSubscribes ?? [], + autoSubscribeLog: props.autoSubscribeLog ?? false, + doNotLoadACL: props.doNotLoadACL ?? true, + doNotLoadAllObjects: props.doNotLoadAllObjects ?? true, + }; + } + props; + ignoreState = ''; + connected = false; + subscribed = false; + firstConnect = true; + waitForRestart = false; + loaded = false; + simStates = {}; + statesSubscribes = {}; + filesSubscribes = {}; + objectsSubscribes = {}; + objects = {}; + states = {}; + acl = null; + isSecure = false; + // Do not inform about readiness two times + onReadyDone = false; + onConnectionHandlers = []; + onLogHandlers = []; + onCmdStdoutHandler; + onCmdStderrHandler; + onCmdExitHandler; + onError(error) { + (this.props.onError ?? console.error)(error); + } + /** The socket instance */ + _socket; + _waitForSocketPromise; + _waitForFirstConnectionPromise = createDeferredPromise(); + /** array with all subscriptions to instances */ + _instanceSubscriptions = {}; + /** Cache for server requests */ + _promises = {}; + _authTimer; + _systemConfig; + /** The "system.config" object */ + get systemConfig() { + return this._systemConfig; + } + /** System language. It could be changed during runtime */ + systemLang = 'en'; + /** + * Checks if this connection is running in a web adapter and not in an admin. + * + * @returns True if running in a web adapter or in a socketio adapter. + */ + static isWeb() { + return window.socketUrl !== undefined; + } + waitForSocketLib() { + // Only wait once + if (this._waitForSocketPromise) { + return this._waitForSocketPromise; + } + // eslint-disable-next-line no-async-promise-executor + this._waitForSocketPromise = new Promise(async (resolve, reject) => { + // If socket io is not yet loaded, we need to wait for it + if (typeof window.io === 'undefined' && typeof window.iob === 'undefined') { + // If the registerSocketOnLoad function is defined in index.html, + // we can use it to know when the socket library was loaded + if (typeof window.registerSocketOnLoad === 'function') { + window.registerSocketOnLoad(() => resolve()); + } + else { + // otherwise, we need to poll + for (let i = 1; i <= 30; i++) { + if (window.io || window.iob) { + return resolve(); + } + await wait(100); + } + reject(new Error('Socket library could not be loaded!')); + } + } + else { + resolve(); + } + }); + return this._waitForSocketPromise; + } + /** + * Starts the socket.io connection. + */ + async startSocket() { + if (this._socket) { + return; + } + let host = this.props.host; + let port = this.props.port; + let protocol = (this.props.protocol || window.location.protocol).replace(':', ''); + let path = window.location.pathname; + if (window.location.hostname === 'iobroker.net' || window.location.hostname === 'iobroker.pro') { + path = ''; + } + else { + // if web adapter, socket io could be on another port or even host + if (window.socketUrl) { + const parsed = new URL(window.socketUrl); + host = parsed.hostname; + port = parsed.port; + protocol = parsed.protocol.replace(':', ''); + } + // get a current path + const pos = path.lastIndexOf('/'); + if (pos !== -1) { + path = path.substring(0, pos + 1); + } + if (Connection.isWeb()) { + // remove one level, like echarts, vis, .... We have here: '/echarts/' + const parts = path.split('/'); + if (parts.length > 2) { + parts.pop(); + parts.pop(); + // material can have paths like this '/material/1.3.0/', so remove one more level + if (ADAPTERS.includes(parts[parts.length - 1])) { + parts.pop(); + } + path = parts.join('/'); + if (!path.endsWith('/')) { + path += '/'; + } + } + } + } + const url = port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`; + this._socket = (window.io || window.iob).connect(url, { + path: path.endsWith('/') ? `${path}socket.io` : `${path}/socket.io`, + query: 'ws=true', + name: this.props.name, + timeout: this.props.ioTimeout, + uuid: this.props.uuid, + token: this.props.token, + }); + this._socket.on('connect', noTimeout => { + this.onReadyDone = false; + // If the user is not admin, it takes some time to install the handlers, because all rights must be checked + if (noTimeout !== true) { + this.connected = true; + setTimeout(() => this.getVersion() + .then(info => { + const [major, minor, patch] = info.version.split('.'); + const v = parseInt(major, 10) * 10000 + parseInt(minor, 10) * 100 + parseInt(patch, 10); + if (v < 40102) { + this._authTimer = null; + // possible this is an old version of admin + this.onPreConnect(false, false); + } + else { + this._socket.emit('authenticate', (isOk, isSecure) => this.onPreConnect(isOk, isSecure)); + } + }) + .catch(e => this.onError({ + message: e.toString(), + operation: 'getVersion', + })), 500); + } + else { + // iobroker websocket waits, till all handlers are installed + this._socket.emit('authenticate', (isOk, isSecure) => { + this.onPreConnect(isOk, isSecure); + }); + } + }); + this._socket.on('reconnect', () => { + this.onReadyDone = false; + this.props.onProgress?.(PROGRESS.READY); + this.connected = true; + if (this.waitForRestart) { + window.location.reload(); + } + else { + this._subscribe(true); + this.onConnectionHandlers.forEach(cb => cb(true)); + } + }); + this._socket.on('disconnect', () => { + this.onReadyDone = false; + this.connected = false; + this.subscribed = false; + this.props.onProgress?.(PROGRESS.CONNECTING); + this.onConnectionHandlers.forEach(cb => cb(false)); + }); + this._socket.on('reauthenticate', () => this.authenticate()); + this._socket.on('log', (message) => { + this.props.onLog?.(message); + this.onLogHandlers.forEach(cb => cb(message)); + }); + this._socket.on('error', (err) => { + let _err; + if (err == undefined) { + _err = ''; + } + else if (typeof err.toString === 'function') { + _err = err.toString(); + } + else { + _err = JSON.stringify(err); + console.error(`Received strange error: ${_err}`); + } + if (_err.includes('User not authorized')) { + this.authenticate(); + } + else if (_err.includes('websocket error')) { + console.error(`Socket Error => reload: ${err}`); + window.location.reload(); + } + else { + console.error(`Socket Error: ${err}`); + } + }); + this._socket.on('connect_error', (err) => console.error(`Connect error: ${err}`)); + this._socket.on('permissionError', err => this.onError({ + message: 'no permission', + operation: err.operation, + type: err.type, + id: err.id || '', + })); + this._socket.on('objectChange', (id, obj) => { + setTimeout(() => this.objectChange(id, obj), 0); + }); + this._socket.on('stateChange', (id, state) => { + setTimeout(() => this.stateChange(id, state), 0); + }); + // instance message + this._socket.on('im', (messageType, from, data) => { + setTimeout(() => this.instanceMessage(messageType, from, data), 0); + }); + this._socket.on('fileChange', (id, fileName, size) => { + setTimeout(() => this.fileChange(id, fileName, size), 0); + }); + this._socket.on('cmdStdout', (id, text) => { + this.onCmdStdoutHandler?.(id, text); + }); + this._socket.on('cmdStderr', (id, text) => { + this.onCmdStderrHandler?.(id, text); + }); + this._socket.on('cmdExit', (id, exitCode) => { + this.onCmdExitHandler?.(id, exitCode); + }); + return Promise.resolve(); + } + /** + * Called internally. + */ + onPreConnect(_isOk, isSecure) { + if (this._authTimer) { + clearTimeout(this._authTimer); + this._authTimer = null; + } + this.connected = true; + this.isSecure = isSecure; + if (this.waitForRestart) { + window.location.reload(); + } + else { + if (this.firstConnect) { + void this.loadData().catch(e => console.error(`Cannot load data: ${e}`)); + } + else { + this.props.onProgress?.(PROGRESS.READY); + } + this._subscribe(true); + this.onConnectionHandlers.forEach(cb => cb(true)); + } + this._waitForFirstConnectionPromise.resolve(); + } + /** + * Checks if running in ioBroker cloud + */ + static isCloud() { + if (window.location.hostname.includes('amazonaws.com') || window.location.hostname.includes('iobroker.in')) { + return true; + } + if (typeof window.socketUrl === 'undefined') { + return false; + } + return window.socketUrl.includes('iobroker.in') || window.socketUrl.includes('amazonaws'); + } + /** + * Checks if the socket is connected. + * + * @returns true if connected. + */ + isConnected() { + return this.connected; + } + /** + * Returns a promise which is resolved when the socket is connected. + */ + waitForFirstConnection() { + return this._waitForFirstConnectionPromise; + } + /** + * Called internally. + */ + async getUserPermissions() { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getUserPermissions', (err, acl) => { + if (err) { + reject(err); + } + else { + resolve(acl); + } + }); + }, + }); + } + /** Loads the important data and retries a couple of times if it takes too long */ + async loadData() { + if (this.loaded) { + return; + } + const maxAttempts = 10; + for (let i = 1; i <= maxAttempts; i++) { + void this.doLoadData().catch(e => console.error(`Cannot load data: ${e}`)); + if (this.loaded) { + return; + } + // give more time via remote connection + await wait(Connection.isCloud() ? 5000 : 1000); + } + } + /** + * Called after the socket is connected. Loads the necessary data. + */ + async doLoadData() { + if (this.loaded) { + return; + } + // Load ACL if not disabled + if (!this.props.doNotLoadACL) { + try { + this.acl = await this.getUserPermissions(); + } + catch (e) { + this.onError(`Cannot read user permissions: ${e}`); + return; + } + } + // Load system config if not disabled + try { + if (this.props.admin5only && + !Connection.isWeb() && + (!window.vendorPrefix || window.vendorPrefix === '@@vendorPrefix@@')) { + this._systemConfig = await this.getCompactSystemConfig(); + } + else { + this._systemConfig = await this.getSystemConfig(); + } + } + catch (e) { + this.onError(`Cannot read system config: ${e}`); + return; + } + // Detect the system language + if (this._systemConfig) { + this.systemLang = this._systemConfig.common?.language; + if (!this.systemLang) { + this.systemLang = (window.navigator.userLanguage || window.navigator.language); + // Browsers may report languages like "de-DE", "en-US", etc. + // ioBroker expects "de", "en", ... + if (/^(en|de|ru|pt|nl|fr|it|es|pl|uk)-?/.test(this.systemLang)) { + this.systemLang = this.systemLang.substring(0, 2); + } + else if (!/^(en|de|ru|pt|nl|fr|it|es|pl|uk|zh-cn)$/.test(this.systemLang)) { + this.systemLang = 'en'; + } + this._systemConfig.common.language = this.systemLang; + } + } + this.props.onLanguage?.(this.systemLang); + // We are now connected + this.loaded = true; + this.props.onProgress?.(PROGRESS.CONNECTED); + this.firstConnect = false; + // Load all objects if desired + if (!this.props.doNotLoadAllObjects) { + this.objects = await this.getObjects(); + } + else if (this.props.admin5only) { + this.objects = {}; + } + else { + this.objects = { 'system.config': this._systemConfig }; + } + this.props.onProgress?.(PROGRESS.READY); + if (!this.onReadyDone) { + this.onReadyDone = true; + this.props.onReady?.(this.objects); + } + } + /** + * Called internally. + */ + authenticate() { + if (window.location.search.includes('&href=')) { + window.location.href = `${window.location.protocol}//${window.location.host}${window.location.pathname}${window.location.search}${window.location.hash}`; + } + else { + window.location.href = `${window.location.protocol}//${window.location.host}${window.location.pathname}?login&href=${window.location.search}${window.location.hash}`; + } + } + async subscribeState(...args) { + let id; + let binary; + let cb; + if (args.length === 3) { + [id, binary, cb] = args; + } + else { + [id, cb] = args; + binary = false; + } + let ids; + if (!Array.isArray(id)) { + ids = [id]; + } + else { + ids = id; + } + if (typeof cb !== 'function') { + throw new Error('The state change handler must be a function!'); + } + const toSubscribe = []; + for (let i = 0; i < ids.length; i++) { + const _id = ids[i]; + if (!this.statesSubscribes[_id]) { + this.statesSubscribes[_id] = { + reg: new RegExp(pattern2RegEx(_id)), + cbs: [cb], + }; + if (id !== this.ignoreState) { + toSubscribe.push(_id); + } + } + else { + !this.statesSubscribes[_id].cbs.includes(cb) && this.statesSubscribes[_id].cbs.push(cb); + } + } + if (!this.connected) { + return; + } + if (toSubscribe.length) { + // no answer from server required + this._socket.emit('subscribe', toSubscribe); + } + // Try to get the current value(s) of the state(s) and call the change handlers + if (binary) { + let base64; + for (let i = 0; i < ids.length; i++) { + try { + // binary states are deprecated + base64 = await this.getBinaryState(ids[i]); + } + catch (e) { + console.error(`Cannot getBinaryState "${ids[i]}": ${JSON.stringify(e)}`); + base64 = undefined; + } + if (base64 != undefined) { + cb(ids[i], base64); + } + } + } + else if (ids.find(_id => _id.includes('*'))) { + let states; + for (let i = 0; i < ids.length; i++) { + try { + states = await this.getForeignStates(ids[i]); + } + catch (e) { + console.error(`Cannot getForeignStates "${ids[i]}": ${JSON.stringify(e)}`); + return; + } + if (states) { + for (const [id, state] of Object.entries(states)) { + const mayBePromise = cb(id, state); + if (mayBePromise instanceof Promise) { + void mayBePromise.catch(e => console.error(`Cannot call state change handler: ${e}`)); + } + } + } + } + } + else { + try { + const states = await (Connection.isWeb() ? this.getStates(ids) : this.getForeignStates(ids)); + if (states) { + for (const [id, state] of Object.entries(states)) { + const mayBePromise = cb(id, state); + if (mayBePromise instanceof Promise) { + void mayBePromise.catch(e => console.error(`Cannot call state change handler: ${e}`)); + } + } + } + } + catch (e) { + console.error(`Cannot getState "${ids.join(', ')}": ${e.message}`); + return; + } + } + } + /** + * Subscribe to the changes of the given state and wait for answer. + * + * @param id The ioBroker state ID. + * @param cb The callback. + */ + async subscribeStateAsync(id, cb) { + return this.subscribeState(id, cb); + } + /** + * Unsubscribes the given callback from changes of the given state. + * + * @param id The ioBroker state ID or array of state IDs. + * @param cb The callback. + */ + unsubscribeState(id, cb) { + let ids; + if (!Array.isArray(id)) { + ids = [id]; + } + else { + ids = id; + } + const toUnsubscribe = []; + for (let i = 0; i < ids.length; i++) { + const _id = ids[i]; + if (this.statesSubscribes[_id]) { + const sub = this.statesSubscribes[_id]; + if (cb) { + const pos = sub.cbs.indexOf(cb); + pos !== -1 && sub.cbs.splice(pos, 1); + } + else { + sub.cbs = []; + } + if (!sub.cbs?.length) { + delete this.statesSubscribes[_id]; + if (_id !== this.ignoreState) { + toUnsubscribe.push(_id); + } + } + } + } + if (this.connected && toUnsubscribe.length) { + this._socket.emit('unsubscribe', ids); + } + } + /** + * Subscribe to changes of the given object. + * In compare to the subscribeState method, + * this method does not call the handler with the current value immediately after subscribe. + * + * the current value. + * + * @param id The ioBroker object ID. + * @param cb The callback. + */ + subscribeObject(id, cb) { + let ids; + if (!Array.isArray(id)) { + ids = [id]; + } + else { + ids = id; + } + if (typeof cb !== 'function') { + throw new Error('The object change handler must be a function!'); + } + const toSubscribe = []; + for (let i = 0; i < ids.length; i++) { + const _id = ids[i]; + if (!this.objectsSubscribes[_id]) { + this.objectsSubscribes[_id] = { + reg: new RegExp(pattern2RegEx(_id)), + cbs: [cb], + }; + toSubscribe.push(_id); + } + else { + !this.objectsSubscribes[_id].cbs.includes(cb) && this.objectsSubscribes[_id].cbs.push(cb); + } + } + if (this.connected && toSubscribe.length) { + this._socket.emit('subscribeObjects', toSubscribe); + } + return Promise.resolve(); + } + /** + * Unsubscribes all callbacks from changes of the given object. + * + * @param id The ioBroker object ID. + */ + /** + * Unsubscribes the given callback from changes of the given object. + * + * @param id The ioBroker object ID. + * @param cb The callback. + */ + unsubscribeObject(id, cb) { + let ids; + if (!Array.isArray(id)) { + ids = [id]; + } + else { + ids = id; + } + const toUnsubscribe = []; + for (let i = 0; i < ids.length; i++) { + const _id = ids[i]; + if (this.objectsSubscribes[_id]) { + const sub = this.objectsSubscribes[_id]; + if (cb) { + const pos = sub.cbs.indexOf(cb); + pos !== -1 && sub.cbs.splice(pos, 1); + } + else { + sub.cbs = []; + } + if (!sub.cbs?.length) { + delete this.objectsSubscribes[_id]; + toUnsubscribe.push(_id); + } + } + } + if (this.connected && toUnsubscribe.length) { + this._socket.emit('unsubscribeObjects', toUnsubscribe); + } + return Promise.resolve(); + } + /** + * Called internally. + * + * @param id The ioBroker object ID. + * @param obj The new object. + */ + objectChange(id, obj) { + // update main.objects cache + // Remember the id and type of th old object + let oldObj; + if (this.objects[id]) { + oldObj = { _id: id, type: this.objects[id].type }; + } + let changed = false; + if (obj) { + // The object was added, updated or changed + // Copy the _rev property (whatever that is) + if (obj._rev && this.objects[id]) { + this.objects[id]._rev = obj._rev; + } + // Detect if there was a change + if (!this.objects[id] || JSON.stringify(this.objects[id]) !== JSON.stringify(obj)) { + this.objects[id] = obj; + changed = true; + } + } + else if (this.objects[id]) { + // The object was deleted + delete this.objects[id]; + changed = true; + } + // Notify all subscribed listeners + for (const [_id, sub] of Object.entries(this.objectsSubscribes)) { + if (_id === id || sub.reg.test(id)) { + sub.cbs.forEach(cb => { + try { + const mayBePromise = cb(id, obj, oldObj); + if (mayBePromise instanceof Promise) { + void mayBePromise.catch(e => console.error(`Cannot call object change handler: ${e}`)); + } + } + catch (e) { + console.error(`Error by callback of objectChange: ${e}`); + } + }); + } + } + // Notify the default listener on change + if (changed) { + const mayBePromise = this.props.onObjectChange?.(id, obj); + if (mayBePromise instanceof Promise) { + void mayBePromise.catch(e => console.error(`Cannot call object change handler: ${e}`)); + } + } + } + /** + * Called internally. + * + * @param id The ioBroker state ID. + * @param state The new state value. + */ + stateChange(id, state) { + for (const sub of Object.values(this.statesSubscribes)) { + if (sub.reg.test(id)) { + for (const cb of sub.cbs) { + try { + const mayBePromise = cb(id, (state ?? null)); + if (mayBePromise instanceof Promise) { + void mayBePromise.catch(e => console.error(`Cannot call state change handler: ${e}`)); + } + } + catch (e) { + console.error(`Error by callback of stateChanged: ${e}`); + } + } + } + } + } + /** + * Called internally. + * + * @param messageType The message type from the instance + * @param sourceInstance The source instance + * @param data The message data + */ + instanceMessage(messageType, sourceInstance, data) { + this._instanceSubscriptions[sourceInstance]?.forEach(sub => { + if (sub.messageType === messageType) { + const mayBePromise = sub.callback(data, sourceInstance, messageType); + if (mayBePromise instanceof Promise) { + void mayBePromise.catch(e => console.error(`Cannot call instance message handler: ${e}`)); + } + } + }); + } + /** + * Called internally. + * + * @param id The ioBroker object ID of type 'meta'. + * @param fileName - file name + * @param size - size of the file + */ + fileChange(id, fileName, size) { + for (const sub of Object.values(this.filesSubscribes)) { + if (sub.regId.test(id) && sub.regFilePattern.test(fileName)) { + for (const cb of sub.cbs) { + try { + cb(id, fileName, size); + } + catch (e) { + console.error(`Error by callback of fileChange: ${e}`); + } + } + } + } + } + /** + * Subscribe to changes of the files. + * + * @param id The ioBroker state ID for a "meta" object. Could be a pattern + * @param filePattern Pattern or file name, like 'main/*' or 'main/visViews.json` + * @param cb The callback. + */ + async subscribeFiles(id, filePattern, cb) { + if (typeof cb !== 'function') { + throw new Error('The state change handler must be a function!'); + } + let filePatterns; + if (Array.isArray(filePattern)) { + filePatterns = filePattern; + } + else { + filePatterns = [filePattern]; + } + const toSubscribe = []; + for (let f = 0; f < filePatterns.length; f++) { + const pattern = filePatterns[f]; + const key = `${id}$%$${pattern}`; + if (!this.filesSubscribes[key]) { + this.filesSubscribes[key] = { + regId: new RegExp(pattern2RegEx(id)), + regFilePattern: new RegExp(pattern2RegEx(pattern)), + cbs: [cb], + }; + toSubscribe.push(pattern); + } + else { + !this.filesSubscribes[key].cbs.includes(cb) && this.filesSubscribes[key].cbs.push(cb); + } + } + if (this.connected && toSubscribe.length) { + this._socket.emit('subscribeFiles', id, toSubscribe); + } + return Promise.resolve(); + } + /** + * Unsubscribes the given callback from changes of files. + * + * @param id The ioBroker state ID. + * @param filePattern Pattern or file name, like 'main/*' or 'main/visViews.json` + * @param cb The callback. + */ + unsubscribeFiles(id, filePattern, cb) { + let filePatterns; + if (Array.isArray(filePattern)) { + filePatterns = filePattern; + } + else { + filePatterns = [filePattern]; + } + const toUnsubscribe = []; + for (let f = 0; f < filePatterns.length; f++) { + const pattern = filePatterns[f]; + const key = `${id}$%$${pattern}`; + if (this.filesSubscribes[key]) { + const sub = this.filesSubscribes[key]; + if (cb) { + const pos = sub.cbs.indexOf(cb); + pos !== -1 && sub.cbs.splice(pos, 1); + } + else { + sub.cbs = []; + } + if (!sub.cbs?.length) { + delete this.filesSubscribes[key]; + toUnsubscribe.push(pattern); + } + } + } + if (this.connected && toUnsubscribe.length) { + this._socket.emit('unsubscribeFiles', id, toUnsubscribe); + } + } + /** Requests data from the server or reads it from the cache */ + async request({ cacheKey, forceUpdate, commandTimeout, onTimeout, requireAdmin, requireFeatures, + // requestName, + executor, }) { + // TODO: mention requestName in errors + // If the command requires the admin adapter, enforce it + if (requireAdmin && Connection.isWeb()) { + return Promise.reject(new Error(ERRORS.NOT_ADMIN)); + } + // Return the cached value if allowed + if (cacheKey && !forceUpdate && cacheKey in this._promises) { + return this._promises[cacheKey]; + } + // Require the socket to be connected + if (!this.connected) { + return Promise.reject(new Error(ERRORS.NOT_CONNECTED)); + } + // Check if all required features are supported + if (requireFeatures?.length) { + for (const feature of requireFeatures) { + if (!(await this.checkFeatureSupported(feature))) { + throw new Error(ERRORS.NOT_SUPPORTED); + } + } + } + // eslint-disable-next-line no-async-promise-executor + const promise = new Promise(async (resolve, reject) => { + const timeoutControl = { + elapsed: false, + clearTimeout: () => { + // no-op unless there is a timeout + }, + }; + let timeout; + if (commandTimeout !== false) { + timeout = setTimeout(() => { + timeoutControl.elapsed = true; + // Let the caller know that the timeout elapsed + onTimeout?.(); + // do not cache responses with timeout or no connection + if (cacheKey && this._promises[cacheKey] instanceof Promise) { + delete this._promises[cacheKey]; + } + reject(new Error(ERRORS.TIMEOUT)); + }, commandTimeout ?? this.props.cmdTimeout); + timeoutControl.clearTimeout = () => { + clearTimeout(timeout); + }; + } + // Call the actual function - awaiting it allows us to catch sync and async errors + // no matter if the executor is async or not + try { + await executor(resolve, reject, timeoutControl); + } + catch (e) { + // do not cache responses with timeout or no connection + if (cacheKey && this._promises[cacheKey] instanceof Promise) { + delete this._promises[cacheKey]; + } + reject(new Error(e.toString())); + } + }); + if (cacheKey) { + this._promises[cacheKey] = promise; + } + return promise; + } + /** + * Deletes cached promise. + * So next time the information will be requested anew + */ + resetCache(key, isAll) { + if (isAll) { + Object.keys(this._promises) + .filter(k => k.startsWith(key)) + .forEach(k => { + delete this._promises[k]; + }); + } + else { + delete this._promises[key]; + } + } + /** + * Gets all states. + * + * @param pattern Pattern of states or array of IDs + */ + getStates(pattern) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getStates', pattern, (err, res) => { + this.states = res ?? {}; + // if (!disableProgressUpdate) { + // this.props.onProgress?.(PROGRESS.STATES_LOADED); + // } + if (err) { + reject(err); + } + else { + resolve(this.states); + } + }); + }, + }); + } + /** + * Gets the given state. + * + * @param id The state ID. + */ + getState(id) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + if (id && id === this.ignoreState) { + resolve(this.simStates[id] || { val: null, ack: true }); + return; + } + this._socket.emit('getState', id, (err, state) => { + if (err) { + reject(err); + } + else { + resolve(state); + } + }); + }, + }); + } + /** + * Gets the given binary state Base64 encoded. + * + * @deprecated since js-controller 5.0. Use files instead. + * @param id The state ID. + */ + getBinaryState(id) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getBinaryState', id, (err, state) => { + if (err) { + reject(err); + } + else { + resolve(state); + } + }); + }, + }); + } + /** + * Sets the given binary state. + * + * @deprecated since js-controller 5.0. Use files instead. + * @param id The state ID. + * @param base64 The Base64 encoded binary data. + */ + setBinaryState(id, base64) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('setBinaryState', id, base64, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Sets the given state value. + * + * @param id The state ID. + * @param val The state value. + * @param ack Acknowledgement flag. + */ + setState(id, val, ack) { + if (typeof ack === 'boolean') { + val = { val: val, ack }; + } + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + // extra handling for "nothing_selected" state for vis + if (id && id === this.ignoreState) { + let state; + if (typeof ack === 'boolean') { + state = val; + } + else if (typeof val === 'object' && val.val !== undefined) { + state = val; + } + else { + state = { + val: val, + ack: false, + ts: Date.now(), + lc: Date.now(), + from: 'system.adapter.vis.0', + }; + } + this.simStates[id] = state; + // inform subscribers about changes + if (this.statesSubscribes[id]) { + for (const cb of this.statesSubscribes[id].cbs) { + try { + const mayBePromise = cb(id, state); + if (mayBePromise instanceof Promise) { + void mayBePromise.catch(e => console.error(`Cannot call state change handler: ${e}`)); + } + } + catch (e) { + console.error(`Error by callback of stateChanged: ${e}`); + } + } + } + resolve(); + return; + } + this._socket.emit('setState', id, val, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Gets all objects. + * + * @param update Callback that is executed when all objects are retrieved. + */ + /** + * Gets all objects. + * + * @param update Set to true to retrieve all objects from the server (instead of using the local cache). + * @param disableProgressUpdate don't call onProgress() when done + */ + getObjects(update, disableProgressUpdate) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + if (!update && this.objects) { + resolve(this.objects); + return; + } + this._socket.emit(Connection.isWeb() ? 'getObjects' : 'getAllObjects', (err, res) => { + if (!disableProgressUpdate) { + this.props.onProgress?.(PROGRESS.OBJECTS_LOADED); + } + if (err) { + reject(err); + } + else { + this.objects = res ?? {}; + resolve(this.objects); + } + }); + }, + }); + } + /** + * Gets the list of objects by ID. + * + * @param list array of IDs to retrieve + */ + getObjectsById(list) { + return this.request({ + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getObjects', list, (err, res) => { + if (err) { + reject(err); + } + else { + resolve(res); + } + }); + }, + }); + } + /** + * Called internally. + * + * @param isEnable Set to true if subscribing, false to unsubscribe. + */ + _subscribe(isEnable) { + if (isEnable && !this.subscribed) { + this.subscribed = true; + if (this.props.autoSubscribes?.length) { + this._socket.emit('subscribeObjects', this.props.autoSubscribes); + } + // re subscribe objects + const ids = Object.keys(this.objectsSubscribes); + if (ids.length) { + this._socket.emit('subscribeObjects', ids); + } + Object.keys(this.objectsSubscribes).forEach(id => this._socket.emit('subscribeObjects', id)); + // re-subscribe logs + this.props.autoSubscribeLog && this._socket.emit('requireLog', true); + // re subscribe states + Object.keys(this.statesSubscribes).forEach(id => this._socket.emit('subscribe', id)); + // re-subscribe files + Object.keys(this.filesSubscribes).forEach(key => { + const [id, filePattern] = key.split('$%$'); + this._socket.emit('subscribeFiles', id, filePattern); + }); + } + else if (!isEnable && this.subscribed) { + this.subscribed = false; + // un-subscribe objects + if (this.props.autoSubscribes?.length) { + this._socket.emit('unsubscribeObjects', this.props.autoSubscribes); + } + const ids = Object.keys(this.objectsSubscribes); + if (ids.length) { + this._socket.emit('unsubscribeObjects', ids); + } + // un-subscribe logs + this.props.autoSubscribeLog && this._socket.emit('requireLog', false); + // un-subscribe states + Object.keys(this.statesSubscribes).forEach(id => this._socket.emit('unsubscribe', id)); + // re-subscribe files + Object.keys(this.filesSubscribes).forEach(key => { + const [id, filePattern] = key.split('$%$'); + this._socket.emit('unsubscribeFiles', id, filePattern); + }); + } + } + /** + * Requests log updates. + * + * @param isEnabled Set to true to get logs. + */ + requireLog(isEnabled) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('requireLog', isEnabled, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Deletes the given object. + * + * @param id The object ID. + * @param maintenance Force deletion of non conform IDs. + */ + delObject(id, maintenance = false) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('delObject', id, { maintenance }, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Deletes the given object and all its children. + * + * @param id The object ID. + * @param maintenance Force deletion of non conform IDs. + */ + delObjects(id, maintenance) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('delObjects', id, { maintenance }, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Sets the object. + * + * @param id The object ID. + * @param obj The object. + */ + setObject(id, obj) { + if (!obj) { + return Promise.reject(new Error('Null object is not allowed')); + } + obj = JSON.parse(JSON.stringify(obj)); + delete obj.from; + delete obj.user; + delete obj.ts; + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('setObject', id, obj, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Gets the object with the given id from the server. + * + * @param id The object ID. + * @returns The object. + */ + getObject(id) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + if (id && id === this.ignoreState) { + resolve({ + _id: this.ignoreState, + type: 'state', + common: { + name: 'ignored state', + type: 'mixed', + }, + }); + return; + } + this._socket.emit('getObject', id, (err, obj) => { + if (err) { + reject(err); + } + else { + resolve(obj); + } + }); + }, + }); + } + /** + * Sends a message to a specific instance or all instances of some specific adapter. + * + * @param instance The instance to send this message to. + * @param command Command name of the target instance. + * @param data The message data to send. + */ + sendTo(instance, command, data) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: resolve => { + this._socket.emit('sendTo', instance, command, data, (result) => { + resolve(result); + }); + }, + }); + } + /** + * Extend an object and create it if it might not exist. + * + * @param id The id. + * @param obj The object. + */ + extendObject(id, obj) { + if (!obj) { + return Promise.reject(new Error('Null object is not allowed')); + } + obj = JSON.parse(JSON.stringify(obj)); + delete obj.from; + delete obj.user; + delete obj.ts; + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('extendObject', id, obj, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Register a handler for log messages. + * + * @param handler The handler. + */ + registerLogHandler(handler) { + if (!this.onLogHandlers.includes(handler)) { + this.onLogHandlers.push(handler); + } + } + /** + * Unregister a handler for log messages. + * + * @param handler The handler. + */ + unregisterLogHandler(handler) { + const pos = this.onLogHandlers.indexOf(handler); + pos !== -1 && this.onLogHandlers.splice(pos, 1); + } + /** + * Register a handler for the connection state. + * + * @param handler The handler. + */ + registerConnectionHandler(handler) { + if (!this.onConnectionHandlers.includes(handler)) { + this.onConnectionHandlers.push(handler); + } + } + /** + * Unregister a handler for the connection state. + * + * @param handler The handler. + */ + unregisterConnectionHandler(handler) { + const pos = this.onConnectionHandlers.indexOf(handler); + pos !== -1 && this.onConnectionHandlers.splice(pos, 1); + } + /** + * Set the handler for standard output of a command. + * + * @param handler The handler. + */ + registerCmdStdoutHandler(handler) { + this.onCmdStdoutHandler = handler; + } + /** + * Unset the handler for standard output of a command. + */ + unregisterCmdStdoutHandler() { + this.onCmdStdoutHandler = undefined; + } + /** + * Set the handler for standard error of a command. + * + * @param handler The handler. + */ + registerCmdStderrHandler(handler) { + this.onCmdStderrHandler = handler; + } + /** + * Unset the handler for standard error of a command. + */ + unregisterCmdStderrHandler() { + this.onCmdStderrHandler = undefined; + } + /** + * Set the handler for exit of a command. + * + * @param handler The handler. + */ + registerCmdExitHandler(handler) { + this.onCmdExitHandler = handler; + } + /** + * Unset the handler for exit of a command. + */ + unregisterCmdExitHandler() { + this.onCmdExitHandler = undefined; + } + /** + * Get all enums with the given name. + * + * @param _enum The name of the enum, like `rooms` or `functions` + * @param update Force update. + */ + getEnums(_enum, update) { + return this.request({ + cacheKey: `enums_${_enum || 'all'}`, + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getObjectView', 'system', 'enum', { + startkey: `enum.${_enum || ''}`, + endkey: _enum ? `enum.${_enum}.\u9999` : `enum.\u9999`, + }, (err, res) => { + if (err) { + reject(err); + } + else { + const _res = {}; + if (res) { + for (let i = 0; i < res.rows.length; i++) { + if (_enum && res.rows[i].id === `enum.${_enum}`) { + continue; + } + _res[res.rows[i].id] = res.rows[i].value; + } + } + resolve(_res); + } + }); + }, + }); + } + /** + * @deprecated since version 1.1.15, cause parameter order does not match backend + * Query a predefined object view. + * @param start The start ID. + * @param end The end ID. + * @param type The type of object. + */ + getObjectView(start, end, type) { + return this.getObjectViewCustom('system', type, start, end); + } + /** + * Query a predefined object view. + * + * @param type The type of object. + * @param start The start ID. + * @param [end] The end ID. + */ + getObjectViewSystem(type, start, end) { + return this.getObjectViewCustom('system', type, start, end); + } + /** + * Query a predefined object view. + * + * @param design design - 'system' or other designs like `custom`. + * @param type The type of object. + * @param start The start ID. + * @param [end] The end ID. + */ + getObjectViewCustom(design, type, start, end) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + start = start || ''; + end = end || '\u9999'; + this._socket.emit('getObjectView', design, type, { startkey: start, endkey: end }, (err, res) => { + if (err) { + reject(err); + } + else { + const _res = {}; + if (res && res.rows) { + for (let i = 0; i < res.rows.length; i++) { + _res[res.rows[i].id] = res.rows[i].value; + } + } + resolve(_res); + } + }); + }, + }); + } + /** + * Read the meta items. + */ + readMetaItems() { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getObjectView', 'system', 'meta', { startkey: '', endkey: '\u9999' }, (err, objs) => { + if (err) { + reject(err); + } + else { + resolve(objs.rows?.map(obj => obj.value).filter((val) => !!val)); + } + }); + }, + }); + } + /** + * Read the directory of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param path The directory name. + */ + readDir(namespace, path) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('readDir', namespace, path, (err, files) => { + if (err) { + reject(err); + } + else { + resolve(files); + } + }); + }, + }); + } + /** + * Read a file of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param fileName The file name. + * @param base64 If it must be a base64 format + */ + readFile(namespace, fileName, base64) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit(base64 ? 'readFile64' : 'readFile', namespace, fileName, (err, data, type) => { + if (err) { + reject(err); + } + else { + resolve({ file: data, mimeType: type }); + } + }); + }, + }); + } + /** + * Write a file of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param fileName The file name. + * @param data The data (if it's a Buffer, it will be converted to Base64). + */ + writeFile64(namespace, fileName, data) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + if (typeof data === 'string') { + this._socket.emit('writeFile', namespace, fileName, data, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + } + else { + const base64 = btoa(new Uint8Array(data).reduce((data, byte) => data + String.fromCharCode(byte), '')); + this._socket.emit('writeFile64', namespace, fileName, base64, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + } + }, + }); + } + /** + * Delete a file of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param fileName The file name. + */ + deleteFile(namespace, fileName) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('deleteFile', namespace, fileName, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Delete a folder of an adapter. + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param folderName The folder name. + */ + deleteFolder(namespace, folderName) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('deleteFolder', namespace, folderName, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Rename file or folder in ioBroker DB + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param oldName current file name, e.g., main/vis-views.json + * @param newName new file name, e.g., main/vis-views-new.json + */ + rename(namespace, oldName, newName) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('rename', namespace, oldName, newName, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Rename file in ioBroker DB + * + * @param namespace (this may be the adapter name, the instance name or the name of a storage object within the adapter). + * @param oldName current file name, e.g., main/vis-views.json + * @param newName new file name, e.g., main/vis-views-new.json + */ + renameFile(namespace, oldName, newName) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('renameFile', namespace, oldName, newName, err => { + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Execute a command on a host. + */ + cmdExec( + /** Host name */ + host, + /** Command to execute */ + cmd, + /** Command ID */ + cmdId, + /** Timeout of command in ms */ + cmdTimeout) { + return this.request({ + commandTimeout: cmdTimeout, + executor: (resolve, reject, timeout) => { + host = normalizeHostId(host); + this._socket.emit('cmdExec', host, cmdId, cmd, err => { + if (timeout.elapsed) { + return; + } + timeout.clearTimeout(); + if (err) { + reject(err); + } + else { + resolve(); + } + }); + }, + }); + } + /** + * Gets the system configuration. + * + * @param update Force update. + */ + getSystemConfig(update) { + return this.request({ + cacheKey: 'systemConfig', + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: async (resolve) => { + let systemConfig = await this.getObject('system.config'); + systemConfig ??= {}; + systemConfig.common ??= {}; + systemConfig.native ??= {}; + resolve(systemConfig); + }, + }); + } + // returns very optimized information for adapters to minimize a connection load + getCompactSystemConfig(update) { + return this.request({ + cacheKey: 'systemConfigCommon', + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + requireAdmin: true, + executor: (resolve, reject) => { + this._socket.emit('getCompactSystemConfig', (err, systemConfig) => { + if (err) { + reject(err); + } + else { + systemConfig ??= {}; + systemConfig.common ??= {}; + systemConfig.native ??= {}; + resolve(systemConfig); + } + }); + }, + }); + } + /** + * Read all states (which might not belong to this adapter) which match the given pattern. + * + * @param pattern The pattern to match. + */ + getForeignStates(pattern) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getForeignStates', pattern || '*', (err, states) => { + if (err) { + reject(err); + } + else { + resolve(states ?? {}); + } + }); + }, + }); + } + /** + * Get foreign objects by pattern, by specific type and resolve their enums. + * + * @param pattern The pattern to match. + * @param type The type of the object. + */ + getForeignObjects(pattern, type) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getForeignObjects', pattern || '*', type, (err, objects) => { + if (err) { + reject(err); + } + else { + resolve(objects); + } + }); + }, + }); + } + /** + * Sets the system configuration. + * + * @param obj The new system configuration. + */ + setSystemConfig(obj) { + return this.setObject('system.config', obj); + } + /** + * Get the raw socket.io socket. + */ + getRawSocket() { + return this._socket; + } + /** + * Get the history of a given state. + * + * @param id The state ID. + * @param options The query options. + */ + getHistory(id, options) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getHistory', id, options, (err, values) => { + if (err) { + reject(err); + } + else { + resolve(values); + } + }); + }, + }); + } + /** + * Get the history of a given state. + * + * @param id The state ID. + * @param options The query options. + */ + getHistoryEx(id, options) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getHistory', id, options, (err, values, step, sessionId) => { + if (err) { + reject(err); + } + else { + resolve({ + values: values, + sessionId: sessionId, + step: step, + }); + } + }); + }, + }); + } + /** + * Get the IP addresses of the given host. + * + * @param host The host name. + * @param update Force update. + */ + getIpAddresses(host, update) { + host = normalizeHostId(host); + return this.request({ + cacheKey: `IPs_${host}`, + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: async (resolve) => { + const obj = await this.getObject(host); + resolve(obj?.common.address ?? []); + }, + }); + } + /** + * Gets the version. + */ + getVersion(update) { + return this.request({ + cacheKey: 'version', + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getVersion', (err, version, serverName) => { + // Old socket.io had no error parameter + if (err && !version && typeof err === 'string' && err.match(/\d+\.\d+\.\d+/)) { + resolve({ version: err, serverName: 'socketio' }); + } + else { + if (err) { + reject(err); + } + else { + resolve({ + version: version, + serverName: serverName, + }); + } + } + }); + }, + }); + } + /** + * Gets the web server name. + */ + getWebServerName() { + return this.request({ + cacheKey: 'webName', + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getAdapterName', (err, name) => { + if (err) { + reject(err); + } + else { + resolve(name); + } + }); + }, + }); + } + /** + * Check if the file exists + * + * @param adapter adapter name + * @param filename file name with the full path. it could be like vis.0/* + */ + fileExists(adapter, filename) { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('fileExists', adapter, filename, (err, exists) => { + if (err) { + reject(err); + } + else { + resolve(!!exists); + } + }); + }, + }); + } + /** + * Read current user + */ + getCurrentUser() { + return this.request({ + // TODO: check if this should time out + commandTimeout: false, + executor: resolve => { + this._socket.emit('authEnabled', (_isSecure, user) => { + resolve(user); + }); + }, + }); + } + /** + * Get uuid + */ + getUuid() { + return this.request({ + cacheKey: 'uuid', + // TODO: check if this should time out + commandTimeout: false, + executor: async (resolve) => { + const obj = await this.getObject('system.meta.uuid'); + resolve(obj?.native?.uuid); + }, + }); + } + /** + * Checks if a given feature is supported. + * + * @param feature The feature to check. + * @param update Force update. + */ + checkFeatureSupported(feature, update) { + return this.request({ + cacheKey: `supportedFeatures_${feature}`, + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('checkFeatureSupported', feature, (err, features) => { + if (err) { + reject(err); + } + else { + resolve(features); + } + }); + }, + }); + } + /** + * Get all adapter instances. + * + * @param update Force update. + */ + /** + * Get all instances of the given adapter. + * + * @param adapter The name of the adapter. + * @param update Force update. + */ + getAdapterInstances(adapter, update) { + if (typeof adapter === 'boolean') { + update = adapter; + adapter = ''; + } + adapter = adapter || ''; + return this.request({ + cacheKey: `instances_${adapter}`, + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: async (resolve) => { + const startKey = adapter ? `system.adapter.${adapter}.` : 'system.adapter.'; + const endKey = `${startKey}\u9999`; + const instances = await this.getObjectViewSystem('instance', startKey, endKey); + const instanceObjects = Object.values(instances); + if (adapter) { + resolve(instanceObjects.filter(o => o.common.name === adapter)); + } + else { + resolve(instanceObjects); + } + }, + }); + } + /** + * Get adapters with the given name. + * + * @param adapter The name of the adapter. + * @param update Force update. + */ + getAdapters(adapter, update) { + if (typeof adapter === 'boolean') { + update = adapter; + adapter = ''; + } + adapter = adapter || ''; + return this.request({ + cacheKey: `adapter_${adapter}`, + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: async (resolve) => { + const adapters = await this.getObjectViewSystem('adapter', `system.adapter.${adapter || ''}`, `system.adapter.${adapter || '\u9999'}`); + const adapterObjects = Object.values(adapters); + if (adapter) { + resolve(adapterObjects.filter(o => o.common.name === adapter)); + } + else { + resolve(adapterObjects); + } + }, + }); + } + /** + * Get the list of all groups. + * + * @param update Force update. + */ + getGroups(update) { + return this.request({ + cacheKey: 'groups', + forceUpdate: update, + // TODO: check if this should time out + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('getObjectView', 'system', 'group', { + startkey: 'system.group.', + endkey: 'system.group.\u9999', + }, (err, doc) => { + if (err) { + reject(err); + } + else { + resolve(getObjectViewResultToArray(doc)); + } + }); + }, + }); + } + /** + * Logout current user + */ + logout() { + return this.request({ + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('logout', err => { + err ? reject(err) : resolve(null); + }); + }, + }); + } + /** + * Subscribe on instance message + * + * @param targetInstance instance, like 'cameras.0' + * @param messageType message type like 'startCamera/cam3' + * @param data optional data object + * @param callback message handler + */ + subscribeOnInstance(targetInstance, messageType, data, callback) { + return this.request({ + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('clientSubscribe', targetInstance, messageType, data, (err, subscribeResult) => { + if (err) { + reject(err); + } + else if (subscribeResult) { + if (subscribeResult.error) { + reject(subscribeResult.error); + } + else { + if (!targetInstance.startsWith('system.adapter.')) { + targetInstance = `system.adapter.${targetInstance}`; + } + // save callback + this._instanceSubscriptions[targetInstance] = + this._instanceSubscriptions[targetInstance] || []; + if (!this._instanceSubscriptions[targetInstance].find(subscription => subscription.messageType === messageType && subscription.callback === callback)) { + this._instanceSubscriptions[targetInstance].push({ + messageType, + callback, + }); + } + resolve(subscribeResult); + } + } + }); + }, + }); + } + /** + * Unsubscribe from instance message + * + * @param targetInstance instance, like 'cameras.0' + * @param messageType message type like 'startCamera/cam3' + * @param callback message handler + */ + unsubscribeFromInstance(targetInstance, messageType, callback) { + if (!targetInstance.startsWith('system.adapter.')) { + targetInstance = `system.adapter.${targetInstance}`; + } + let deleted; + const promiseResults = []; + do { + deleted = false; + const index = this._instanceSubscriptions[targetInstance]?.findIndex(sub => (!messageType || sub.messageType === messageType) && (!callback || sub.callback === callback)); + if (index !== undefined && index !== null && index !== -1) { + deleted = true; + // remember messageType + const _messageType = this._instanceSubscriptions[targetInstance][index].messageType; + this._instanceSubscriptions[targetInstance].splice(index, 1); + if (!this._instanceSubscriptions[targetInstance].length) { + delete this._instanceSubscriptions[targetInstance]; + } + // try to find another subscription for this instance and messageType + const found = this._instanceSubscriptions[targetInstance] && + this._instanceSubscriptions[targetInstance].find(sub => sub.messageType === _messageType); + if (!found) { + promiseResults.push(this.request({ + commandTimeout: false, + executor: (resolve, reject) => { + this._socket.emit('clientUnsubscribe', targetInstance, messageType, (err, wasSubscribed) => (err ? reject(err) : resolve(wasSubscribed))); + }, + })); + } + } + } while (deleted && (!callback || !messageType)); + if (promiseResults.length) { + return Promise.all(promiseResults).then(results => !!results.find(result => result)); + } + return Promise.resolve(false); + } + /** + * Send log to ioBroker log + * + * @param text Log text + * @param level `info`, `debug`, `warn`, `error` or `silly` + */ + log(text, level) { + return text + ? this.request({ + commandTimeout: false, + executor: resolve => { + this._socket.emit('log', text, level); + return resolve(null); + }, + }) + : Promise.resolve(null); + } + /** + * This is a special method for vis. + * It is used to not send to server the changes about "nothing_selected" state + * + * @param id The state that has to be ignored by communication + */ + setStateToIgnore(id) { + this.ignoreState = id; + } +} +//# sourceMappingURL=Connection.js.map \ No newline at end of file diff --git a/example/socket-client/ConnectionProps.d.ts b/example/socket-client/ConnectionProps.d.ts new file mode 100644 index 0000000..4560506 --- /dev/null +++ b/example/socket-client/ConnectionProps.d.ts @@ -0,0 +1,55 @@ +/** + * Log event + */ +export type LogMessage = { + /** Log message */ + message: string; + /** origin */ + from: string; + /** timestamp in ms */ + ts: number; + /** Log message */ + severity: ioBroker.LogLevel; + /** unique ID of the message */ + _id: number; +}; +export interface ConnectionProps { + /** The socket name. */ + name?: string; + /** State IDs to always automatically subscribe to. */ + autoSubscribes?: string[]; + /** Automatically subscribe to logging. */ + autoSubscribeLog?: boolean; + /** The protocol to use for the socket.io connection. */ + protocol?: string; + /** The host name to use for the socket.io connection. */ + host?: string; + /** The port to use for the socket.io connection. */ + port: string | number; + /** The socket.io connection timeout. */ + ioTimeout?: number; + /** The socket.io command timeout. */ + cmdTimeout?: number; + /** Flag to indicate if all objects should be loaded or not. Default true (not loaded) */ + doNotLoadAllObjects?: boolean; + /** Flag to indicate if AccessControlList for current user will be loaded or not. Default true (not loaded) */ + doNotLoadACL?: boolean; + /** Progress callback. */ + onProgress?: (progress: number) => void; + /** Ready callback. */ + onReady?: (objects: Record) => void; + /** Log callback. */ + onLog?: (message: LogMessage) => void; + /** Error callback. */ + onError?: (error: any) => void; + /** Object change callback. */ + onObjectChange?: ioBroker.ObjectChangeHandler; + /** Gets called when the system language is determined */ + onLanguage?: (lang: ioBroker.Languages) => void; + /** Forces the use of the Compact Methods, wich only exists in admin 5 UI. */ + admin5only?: boolean; + /** The device UUID with which the communication must be established */ + uuid?: string; + /** Authentication token (used only in cloud) */ + token?: string; +} diff --git a/example/socket-client/ConnectionProps.js b/example/socket-client/ConnectionProps.js new file mode 100644 index 0000000..3230b47 --- /dev/null +++ b/example/socket-client/ConnectionProps.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=ConnectionProps.js.map \ No newline at end of file diff --git a/example/socket-client/DeferredPromise.d.ts b/example/socket-client/DeferredPromise.d.ts new file mode 100644 index 0000000..9a51764 --- /dev/null +++ b/example/socket-client/DeferredPromise.d.ts @@ -0,0 +1,5 @@ +export interface DeferredPromise extends Promise { + resolve(value: T | PromiseLike): void; + reject(reason?: any): void; +} +export declare function createDeferredPromise(): DeferredPromise; diff --git a/example/socket-client/DeferredPromise.js b/example/socket-client/DeferredPromise.js new file mode 100644 index 0000000..2e25f8e --- /dev/null +++ b/example/socket-client/DeferredPromise.js @@ -0,0 +1,12 @@ +export function createDeferredPromise() { + let res; + let rej; + const promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + promise.resolve = res; + promise.reject = rej; + return promise; +} +//# sourceMappingURL=DeferredPromise.js.map \ No newline at end of file diff --git a/example/socket-client/README.md b/example/socket-client/README.md new file mode 100644 index 0000000..ec9b552 --- /dev/null +++ b/example/socket-client/README.md @@ -0,0 +1,4 @@ +# ioBroker Socket client +This code is taken from here: https://github.com/ioBroker/socket-client + +It is better to use it as a compiled code from "@iobroker/socket-client" \ No newline at end of file diff --git a/example/socket-client/globals.d.ts b/example/socket-client/globals.d.ts new file mode 100644 index 0000000..e7960bc --- /dev/null +++ b/example/socket-client/globals.d.ts @@ -0,0 +1,17 @@ +import type { SocketClient } from './SocketClient.js'; +declare global { + interface Window { + io: { + connect: (name: string, par: any) => SocketClient; + }; + iob: { + connect: (name: string, par: any) => SocketClient; + }; + socketUrl: string; + registerSocketOnLoad: (callback: () => void) => void; + vendorPrefix: string; + } + interface Navigator { + userLanguage: string; + } +} diff --git a/example/socket-client/globals.js b/example/socket-client/globals.js new file mode 100644 index 0000000..1be2b6c --- /dev/null +++ b/example/socket-client/globals.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=globals.js.map \ No newline at end of file diff --git a/example/socket-client/tools.d.ts b/example/socket-client/tools.d.ts new file mode 100644 index 0000000..bc15f2c --- /dev/null +++ b/example/socket-client/tools.d.ts @@ -0,0 +1,12 @@ +export declare function getObjectViewResultToArray(doc: { + rows: ioBroker.GetObjectViewItem[]; +} | undefined): T[]; +/** Makes sure that a host id starts with "system.host." */ +export declare function normalizeHostId(host: string): string; +export declare function objectIdToHostname(id: string): string; +/** + * Creates a promise that waits for the specified time and then resolves + */ +export declare function wait(ms: number): Promise; +/** Converts ioB pattern into regex */ +export declare function pattern2RegEx(pattern: string): string; diff --git a/example/socket-client/tools.js b/example/socket-client/tools.js new file mode 100644 index 0000000..71889ae --- /dev/null +++ b/example/socket-client/tools.js @@ -0,0 +1,33 @@ +export function getObjectViewResultToArray(doc) { + return doc?.rows.map(item => item.value).filter((val) => !!val) ?? []; +} +/** Makes sure that a host id starts with "system.host." */ +export function normalizeHostId(host) { + if (!host?.startsWith('system.host.')) { + host = `system.host.${host}`; + } + return host; +} +export function objectIdToHostname(id) { + if (id?.startsWith('system.host.')) { + id = id.substring('system.host.'.length); + } + return id; +} +/** + * Creates a promise that waits for the specified time and then resolves + */ +export function wait(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} +/** Converts ioB pattern into regex */ +export function pattern2RegEx(pattern) { + pattern = (pattern || '').toString(); + const startsWithWildcard = pattern[0] === '*'; + const endsWithWildcard = pattern[pattern.length - 1] === '*'; + pattern = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*'); + return (startsWithWildcard ? '' : '^') + pattern + (endsWithWildcard ? '' : '$'); +} +//# sourceMappingURL=tools.js.map \ No newline at end of file diff --git a/io-package.json b/io-package.json index b866fdb..c484d2f 100644 --- a/io-package.json +++ b/io-package.json @@ -1,214 +1,206 @@ { - "common": { - "name": "ws", - "version": "2.7.0", - "title": "Web socket", - "titleLang": { - "en": "Web socket" + "common": { + "name": "ws", + "version": "2.7.0", + "title": "Web socket", + "titleLang": { + "en": "Web socket" + }, + "desc": { + "en": "This adapter allows to communicate different web applications with ioBroker", + "de": "Dieser Adapter ermöglicht die Kommunikation verschiedener Web-Anwendungen mit ioBroker", + "ru": "Этот адаптер позволяет соединяться различным веб-приложениям с ioBroker", + "pt": "Este adaptador permite comunicar diferentes aplicativos da web com ioBroker", + "nl": "Met deze adapter kunt u verschillende webtoepassingen communiceren met ioBroker", + "fr": "Cet adaptateur permet de communiquer différentes applications web avec ioBroker", + "it": "Questo adattatore consente di comunicare diverse applicazioni Web con ioBroker", + "es": "Este adaptador permite comunicar diferentes aplicaciones web con ioBroker", + "pl": "Ten adapter umożliwia komunikację z różnymi aplikacjami WWW za pomocą ioBroker", + "uk": "Цей адаптер дозволяє спілкуватися різним веб-додаткам з ioBroker", + "zh-cn": "该适配器允许与ioBroker通信不同的Web应用程序" + }, + "news": { + "2.7.0": { + "en": "Update ws-server library", + "de": "Ws-Server-Bibliothek aktualisieren", + "ru": "Библиотека обновления ws-server", + "pt": "Update ws-server library", + "nl": "Ws-server-bibliotheek bijwerken", + "fr": "Mettre à jour la bibliothèque ws-server", + "it": "Aggiornare la libreria ws-server", + "es": "Actualización ws-servidor biblioteca", + "pl": "Aktualizuj bibliotekę ws- server", + "uk": "Оновлення бібліотеки ws-сервера", + "zh-cn": "更新 ws- 服务器库" + }, + "2.6.2": { + "en": "Corrected call of getObjectView with null parameter", + "de": "Korrigierter Anruf von getObject Ansicht mit Null-Parameter", + "ru": "Исправленный звонок getObject Просмотр с нулевым параметром", + "pt": "Chamada correta de getObject Ver com parâmetro nulo", + "nl": "Gecorrigeerde aanroep van getObject Beeld met nul parameter", + "fr": "Appel corrigé de getObject Affichage avec paramètre null", + "it": "Chiamata corretta di getObject Visualizza con parametro null", + "es": "Llamada corregida de getObject Ver con parámetro null", + "pl": "Poprawione wywołanie getObject Widok z parametrem null", + "uk": "Виправлений виклик getObject Перегляд з параметром null", + "zh-cn": "已更正的调取对象 以无效参数查看" + }, + "2.6.1": { + "en": "fixed require of webserver", + "de": "fester bedarf von webserver", + "ru": "фиксированная потребность веб-сервера", + "pt": "requerimento fixo de servidor web", + "nl": "vaste eis van webserver", + "fr": "demande fixe du serveur web", + "it": "richiesta fissa di webserver", + "es": "requerimiento fijo del servidor web", + "pl": "stałe zapotrzebowanie serwera www", + "uk": "фіксована потреба вебсервера", + "zh-cn": "网页服务器的固定要求" + }, + "2.6.0": { + "en": "use `@iobroker/webserver`", + "de": "`@iobroker/webserver `", + "ru": "`@iobroker/webserver \"", + "pt": "use `@iobroker/webserver \"", + "nl": "gebruik wat", + "fr": "utiliser `@iobroker/webserver \"", + "it": "utilizzare `@iobroker/webserver #", + "es": "use `@iobroker/webserver `", + "pl": "use '@ iobroker / webserver'", + "uk": "використання `@iobroker/webserver й", + "zh-cn": "使用 xqio 经纪人/网络服务器 `" + }, + "2.5.11": { + "en": "Some packages were updated", + "de": "Einige Pakete wurden aktualisiert", + "ru": "Некоторые пакеты были обновлены", + "pt": "Alguns pacotes foram atualizados", + "nl": "Sommige pakketten werden bijgewerkt", + "fr": "Certains paquets ont été mis à jour", + "it": "Alcuni pacchetti sono stati aggiornati", + "es": "Algunos paquetes se actualizaron", + "pl": "Niektóre pakiety zostały zaktualizowane", + "uk": "Деякі пакети були оновлено", + "zh-cn": "一些软件包已更新" + }, + "2.5.10": { + "en": "updated ws-server to increase file limit to 500 MB", + "de": "aktualisiert ws-server, um Dateilimit auf 500 MB zu erhöhen", + "ru": "обновленный ws-сервер для увеличения лимита файлов до 500 МБ", + "pt": "ws-server atualizado para aumentar o limite de arquivo para 500 MB", + "nl": "verhoogde WS-server om de bestandslimiet te verhogen tot 500 MB", + "fr": "mis à jour ws-server pour augmenter la limite de fichiers à 500 Mo", + "it": "aggiornato ws-server per aumentare il limite di file a 500 MB", + "es": "actualizado ws-servidor para aumentar el límite de archivo a 500 MB", + "pl": "uaktualnianie ws-server do zwiększenia limitu plików do 500 MB", + "uk": "оновлений ws-сервер для збільшення ліміту файлу до 500 Мб", + "zh-cn": "a. 更新的保存者将档案限制增加到500 马克" + }, + "2.5.9": { + "en": "Corrected the crash if authentication is enabled", + "de": "Korrigiert den Crash, wenn die Authentifizierung aktiviert ist", + "ru": "Исправлена ошибка, если авторизация включена", + "pt": "Corrigido o acidente se a autenticação estiver ativada", + "nl": "Vertaling:", + "fr": "Correction du crash si l'authentification est activée", + "it": "Corretto il crash se l'autenticazione è abilitata", + "es": "Corregido el accidente si la autenticación está habilitada", + "pl": "Poprawiona katastrofa, jeśli uwierzytelnianie jest możliwe", + "uk": "Виправлено помилку, якщо ввімкнено автентифікацію", + "zh-cn": "如果能够证明确证,便纠正了事故。" + } + }, + "authors": ["bluefox "], + "platform": "Javascript/Node.js", + "mode": "daemon", + "loglevel": "info", + "readme": "https://github.com/ioBroker/ioBroker.ws/blob/master/README.md", + "icon": "ws.svg", + "messagebox": true, + "keywords": ["web", "web socket", "communication"], + "enabled": true, + "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.ws/master/admin/ws.svg", + "type": "communication", + "stopBeforeUpdate": true, + "adminUI": { + "config": "json" + }, + "compact": true, + "stopTimeout": 5000, + "dependencies": [ + { + "js-controller": ">=2.0.0" + } + ], + "globalDependencies": [ + { + "admin": ">=5.0.0" + } + ], + "restartAdapters": ["web"], + "plugins": { + "sentry": { + "dsn": "https://3f4953a5e15d4ff2a381affb010d0778@sentry.iobroker.net/170" + } + }, + "connectionType": "local", + "dataSource": "push", + "tier": 3, + "licenseInformation": { + "type": "free", + "license": "MIT" + } }, - "desc": { - "en": "This adapter allows to communicate different web applications with ioBroker", - "de": "Dieser Adapter ermöglicht die Kommunikation verschiedener Web-Anwendungen mit ioBroker", - "ru": "Этот адаптер позволяет соединяться различным веб-приложениям с ioBroker", - "pt": "Este adaptador permite comunicar diferentes aplicativos da web com ioBroker", - "nl": "Met deze adapter kunt u verschillende webtoepassingen communiceren met ioBroker", - "fr": "Cet adaptateur permet de communiquer différentes applications web avec ioBroker", - "it": "Questo adattatore consente di comunicare diverse applicazioni Web con ioBroker", - "es": "Este adaptador permite comunicar diferentes aplicaciones web con ioBroker", - "pl": "Ten adapter umożliwia komunikację z różnymi aplikacjami WWW za pomocą ioBroker", - "uk": "Цей адаптер дозволяє спілкуватися різним веб-додаткам з ioBroker", - "zh-cn": "该适配器允许与ioBroker通信不同的Web应用程序" + "native": { + "port": 8084, + "auth": false, + "secure": false, + "bind": "0.0.0.0", + "ttl": 3600, + "certPublic": "", + "certPrivate": "", + "certChained": "", + "defaultUser": "admin", + "leEnabled": false, + "leUpdate": false, + "leCheckPort": 80 }, - "news": { - "2.7.0": { - "en": "Update ws-server library", - "de": "Ws-Server-Bibliothek aktualisieren", - "ru": "Библиотека обновления ws-server", - "pt": "Update ws-server library", - "nl": "Ws-server-bibliotheek bijwerken", - "fr": "Mettre à jour la bibliothèque ws-server", - "it": "Aggiornare la libreria ws-server", - "es": "Actualización ws-servidor biblioteca", - "pl": "Aktualizuj bibliotekę ws- server", - "uk": "Оновлення бібліотеки ws-сервера", - "zh-cn": "更新 ws- 服务器库" - }, - "2.6.2": { - "en": "Corrected call of getObjectView with null parameter", - "de": "Korrigierter Anruf von getObject Ansicht mit Null-Parameter", - "ru": "Исправленный звонок getObject Просмотр с нулевым параметром", - "pt": "Chamada correta de getObject Ver com parâmetro nulo", - "nl": "Gecorrigeerde aanroep van getObject Beeld met nul parameter", - "fr": "Appel corrigé de getObject Affichage avec paramètre null", - "it": "Chiamata corretta di getObject Visualizza con parametro null", - "es": "Llamada corregida de getObject Ver con parámetro null", - "pl": "Poprawione wywołanie getObject Widok z parametrem null", - "uk": "Виправлений виклик getObject Перегляд з параметром null", - "zh-cn": "已更正的调取对象 以无效参数查看" - }, - "2.6.1": { - "en": "fixed require of webserver", - "de": "fester bedarf von webserver", - "ru": "фиксированная потребность веб-сервера", - "pt": "requerimento fixo de servidor web", - "nl": "vaste eis van webserver", - "fr": "demande fixe du serveur web", - "it": "richiesta fissa di webserver", - "es": "requerimiento fijo del servidor web", - "pl": "stałe zapotrzebowanie serwera www", - "uk": "фіксована потреба вебсервера", - "zh-cn": "网页服务器的固定要求" - }, - "2.6.0": { - "en": "use `@iobroker/webserver`", - "de": "`@iobroker/webserver `", - "ru": "`@iobroker/webserver \"", - "pt": "use `@iobroker/webserver \"", - "nl": "gebruik wat", - "fr": "utiliser `@iobroker/webserver \"", - "it": "utilizzare `@iobroker/webserver #", - "es": "use `@iobroker/webserver `", - "pl": "use '@ iobroker / webserver'", - "uk": "використання `@iobroker/webserver й", - "zh-cn": "使用 xqio 经纪人/网络服务器 `" - }, - "2.5.11": { - "en": "Some packages were updated", - "de": "Einige Pakete wurden aktualisiert", - "ru": "Некоторые пакеты были обновлены", - "pt": "Alguns pacotes foram atualizados", - "nl": "Sommige pakketten werden bijgewerkt", - "fr": "Certains paquets ont été mis à jour", - "it": "Alcuni pacchetti sono stati aggiornati", - "es": "Algunos paquetes se actualizaron", - "pl": "Niektóre pakiety zostały zaktualizowane", - "uk": "Деякі пакети були оновлено", - "zh-cn": "一些软件包已更新" - }, - "2.5.10": { - "en": "updated ws-server to increase file limit to 500 MB", - "de": "aktualisiert ws-server, um Dateilimit auf 500 MB zu erhöhen", - "ru": "обновленный ws-сервер для увеличения лимита файлов до 500 МБ", - "pt": "ws-server atualizado para aumentar o limite de arquivo para 500 MB", - "nl": "verhoogde WS-server om de bestandslimiet te verhogen tot 500 MB", - "fr": "mis à jour ws-server pour augmenter la limite de fichiers à 500 Mo", - "it": "aggiornato ws-server per aumentare il limite di file a 500 MB", - "es": "actualizado ws-servidor para aumentar el límite de archivo a 500 MB", - "pl": "uaktualnianie ws-server do zwiększenia limitu plików do 500 MB", - "uk": "оновлений ws-сервер для збільшення ліміту файлу до 500 Мб", - "zh-cn": "a. 更新的保存者将档案限制增加到500 马克" - }, - "2.5.9": { - "en": "Corrected the crash if authentication is enabled", - "de": "Korrigiert den Crash, wenn die Authentifizierung aktiviert ist", - "ru": "Исправлена ошибка, если авторизация включена", - "pt": "Corrigido o acidente se a autenticação estiver ativada", - "nl": "Vertaling:", - "fr": "Correction du crash si l'authentification est activée", - "it": "Corretto il crash se l'autenticazione è abilitata", - "es": "Corregido el accidente si la autenticación está habilitada", - "pl": "Poprawiona katastrofa, jeśli uwierzytelnianie jest możliwe", - "uk": "Виправлено помилку, якщо ввімкнено автентифікацію", - "zh-cn": "如果能够证明确证,便纠正了事故。" - } - }, - "authors": [ - "bluefox " - ], - "platform": "Javascript/Node.js", - "mode": "daemon", - "loglevel": "info", - "readme": "https://github.com/ioBroker/ioBroker.ws/blob/master/README.md", - "icon": "ws.png", - "messagebox": true, - "keywords": [ - "web", - "web socket", - "communication" - ], - "enabled": true, - "extIcon": "https://raw.githubusercontent.com/ioBroker/ioBroker.ws/master/admin/ws.png", - "type": "communication", - "stopBeforeUpdate": true, - "adminUI": { - "config": "json" - }, - "compact": true, - "stopTimeout": 5000, - "dependencies": [ - { - "js-controller": ">=2.0.0" - } - ], - "globalDependencies": [ - { - "admin": ">=5.0.0" - } - ], - "restartAdapters": [ - "web" - ], - "plugins": { - "sentry": { - "dsn": "https://3f4953a5e15d4ff2a381affb010d0778@sentry.iobroker.net/170" - } - }, - "connectionType": "local", - "dataSource": "push", - "tier": 3, - "licenseInformation": { - "type": "free", - "license": "MIT" - } - }, - "native": { - "port": 8084, - "auth": false, - "secure": false, - "bind": "0.0.0.0", - "ttl": 3600, - "certPublic": "", - "certPrivate": "", - "certChained": "", - "defaultUser": "admin", - "leEnabled": false, - "leUpdate": false, - "leCheckPort": 80 - }, - "instanceObjects": [ - { - "_id": "info", - "type": "channel", - "common": { - "name": "Information" - }, - "native": {} - }, - { - "_id": "info.connected", - "type": "state", - "common": { - "role": "state", - "name": "Info about connected socket clients", - "type": "string", - "read": true, - "write": false, - "def": "" - }, - "native": {} - }, - { - "_id": "info.connection", - "type": "state", - "common": { - "role": "indicator.connected", - "name": "If web server started", - "type": "boolean", - "read": true, - "write": false, - "def": false - }, - "native": {} - } - ] + "instanceObjects": [ + { + "_id": "info", + "type": "channel", + "common": { + "name": "Information" + }, + "native": {} + }, + { + "_id": "info.connected", + "type": "state", + "common": { + "role": "state", + "name": "Info about connected socket clients", + "type": "string", + "read": true, + "write": false, + "def": "" + }, + "native": {} + }, + { + "_id": "info.connection", + "type": "state", + "common": { + "role": "indicator.connected", + "name": "If web server started", + "type": "boolean", + "read": true, + "write": false, + "def": false + }, + "native": {} + } + ] } diff --git a/lib/passportSocket.js b/lib/passportSocket.js deleted file mode 100644 index 93fc21b..0000000 --- a/lib/passportSocket.js +++ /dev/null @@ -1,100 +0,0 @@ -// Originally taken from here: https://github.com/jfromaniello/passport.socketio/blob/master/lib/index.js -// Copyright Licensed under the MIT-License. 2012-2013 José F. Romaniello. - -function parseCookie(auth, cookieHeader) { - const cookieParser = auth.cookieParser(auth.secret); - const req = { - headers: { - cookie: cookieHeader - } - }; - - let result; - - cookieParser(req, {}, err => { - if (err) { - throw err; - } - result = req.signedCookies || req.cookies; - }); - - return result; -} - -function getQuery(url) { - const query = url.split('?')[1] || ''; - const parts = query.split('&'); - const result = {}; - for (let p = 0; p < parts.length; p++) { - const parts1 = parts[p].split('='); - result[parts1[0]] = parts1[1]; - } - return result; -} - -function authorize(options) { - const defaults = { - key: 'connect.sid', - secret: null, - store: null, - userProperty: 'user' - }; - - const auth = Object.assign({}, defaults, options); - - if (!auth.passport) { - throw new Error('passport is required to use require(\'passport\'), please install passport'); - } - - if (!auth.cookieParser) { - throw new Error('cookieParser is required use require(\'cookie-parser\'), connect.cookieParser or express.cookieParser'); - } - - return function (data, accept) { - data.query = getQuery(data.url); - - if (options.checkUser && data.query.user && data.query.pass) { - return options.checkUser(data.query.user, data.query.pass, (error, result) => { - if (error) { - return auth.fail(data, 'Cannot check user', false, accept); - } else if (!result) { - return auth.fail(data, 'User not found', false, accept); - } else { - data[auth.userProperty] = result; - data[auth.userProperty].logged_in = true; - auth.success(data, accept); - } - }); - } - - data.cookie = parseCookie(auth, data.headers.cookie || ''); - data.sessionID = data.cookie[auth.key] || ''; - data[auth.userProperty] = { - logged_in: false - }; - - auth.store.get(data.sessionID, (err, session) => { - if (err) { - return auth.fail(data, 'Error in session store:\n' + err.message, true, accept); - } else - if (!session) { - return auth.fail(data, 'No session found', false, accept); - } else - if (!session[auth.passport._key]) { - return auth.fail(data, 'Passport was not initialized', true, accept); - } - - const userKey = session[auth.passport._key].user; - - if (typeof userKey === 'undefined') { - return auth.fail(data, 'User not authorized through passport. (User Property not found)', false, accept); - } - - data[auth.userProperty] = userKey; - data[auth.userProperty].logged_in = true; - auth.success(data, accept); - }); - }; -} - -exports.authorize = authorize; diff --git a/lib/socket.js b/lib/socket.js deleted file mode 100644 index 34c782f..0000000 --- a/lib/socket.js +++ /dev/null @@ -1,37 +0,0 @@ -const ws = require('@iobroker/ws-server'); -const SocketCommon = require('@iobroker/socket-classes').SocketCommon; -const SocketWS = require('./socketWS'); - -class Socket { - constructor(server, settings, adapter, ignore, store, checkUser) { - this.ioServer = new SocketWS(settings, adapter); - this.ioServer.start(server, ws, {userKey: 'connect.sid', checkUser, store, secret: settings.secret}); - } - - getWhiteListIpForAddress(remoteIp, whiteListSettings) { - return SocketCommon.getWhiteListIpForAddress(remoteIp, whiteListSettings); - } - - publishAll(type, id, obj) { - return this.ioServer.publishAll(type, id, obj); - } - - publishFileAll(id, fileName, size) { - return this.ioServer.publishFileAll(id, fileName, size); - } - - publishInstanceMessageAll(sourceInstance, messageType, sid, data) { - return this.ioServer.publishInstanceMessageAll(sourceInstance, messageType, sid, data); - } - - sendLog(obj) { - this.ioServer.sendLog(obj); - } - - close() { - this.ioServer.close(); - this.ioServer = null; - } -} - -module.exports = Socket; diff --git a/lib/socketCommands.js b/lib/socketCommands.js deleted file mode 100644 index ec4d097..0000000 --- a/lib/socketCommands.js +++ /dev/null @@ -1,1682 +0,0 @@ -const utils = require('@iobroker/adapter-core'); // Get common adapter utils -const pattern2RegEx = utils.commonTools.pattern2RegEx; -let axios = null; -let zipFiles = null; - -class SocketCommands { - static ERROR_PERMISSION = 'permissionError'; - static COMMANDS_PERMISSIONS = { - getObject: {type: 'object', operation: 'read'}, - getObjects: {type: 'object', operation: 'list'}, - getObjectView: {type: 'object', operation: 'list'}, - setObject: {type: 'object', operation: 'write'}, - requireLog: {type: 'object', operation: 'write'}, // just mapping to some command - delObject: {type: 'object', operation: 'delete'}, - extendObject: {type: 'object', operation: 'write'}, - getHostByIp: {type: 'object', operation: 'list'}, - subscribeObjects: {type: 'object', operation: 'read'}, - unsubscribeObjects: {type: 'object', operation: 'read'}, - - getStates: {type: 'state', operation: 'list'}, - getState: {type: 'state', operation: 'read'}, - setState: {type: 'state', operation: 'write'}, - delState: {type: 'state', operation: 'delete'}, - createState: {type: 'state', operation: 'create'}, - subscribe: {type: 'state', operation: 'read'}, - unsubscribe: {type: 'state', operation: 'read'}, - getStateHistory: {type: 'state', operation: 'read'}, - getVersion: {type: '', operation: ''}, - getAdapterName: {type: '', operation: ''}, - - addUser: {type: 'users', operation: 'create'}, - delUser: {type: 'users', operation: 'delete'}, - addGroup: {type: 'users', operation: 'create'}, - delGroup: {type: 'users', operation: 'delete'}, - changePassword: {type: 'users', operation: 'write'}, - - httpGet: {type: 'other', operation: 'http'}, - cmdExec: {type: 'other', operation: 'execute'}, - sendTo: {type: 'other', operation: 'sendto'}, - sendToHost: {type: 'other', operation: 'sendto'}, - readLogs: {type: 'other', operation: 'execute'}, - - readDir: {type: 'file', operation: 'list'}, - createFile: {type: 'file', operation: 'create'}, - writeFile: {type: 'file', operation: 'write'}, - readFile: {type: 'file', operation: 'read'}, - fileExists: {type: 'file', operation: 'read'}, - deleteFile: {type: 'file', operation: 'delete'}, - readFile64: {type: 'file', operation: 'read'}, - writeFile64: {type: 'file', operation: 'write'}, - unlink: {type: 'file', operation: 'delete'}, - rename: {type: 'file', operation: 'write'}, - mkdir: {type: 'file', operation: 'write'}, - chmodFile: {type: 'file', operation: 'write'}, - chownFile: {type: 'file', operation: 'write'}, - subscribeFiles: {type: 'file', operation: 'read'}, - unsubscribeFiles: {type: 'file', operation: 'read'}, - - authEnabled: {type: '', operation: ''}, - disconnect: {type: '', operation: ''}, - listPermissions: {type: '', operation: ''}, - getUserPermissions: {type: 'object', operation: 'read'} - }; - - constructor(adapter, updateSession) { - this.adapter = adapter; - this.commands = {}; - this.subscribes = {}; - this.logEnabled = false; - this.clientSubscribes = {}; - - this._updateSession = updateSession; - - if (!this._updateSession) { - this._updateSession = () => true; - } - - this._initCommands(); - } - - async _rename(_adapter, oldName, newName, options) { - // read if it is a file or folder - try { - if (oldName.endsWith('/')) { - oldName = oldName.substring(0, oldName.length - 1); - } - - if (newName.endsWith('/')) { - newName = newName.substring(0, newName.length - 1); - } - - const files = await this.adapter.readDirAsync(_adapter, oldName, options); - if (files && files.length) { - for (let f = 0; f < files.length; f++) { - await this._rename(_adapter, `${oldName}/${files[f].file}`, `${newName}/${files[f].file}`); - } - } - } catch (error) { - if (error.message !== 'Not exists') { - throw error; - } - // else ignore, because it is a file and not a folder - } - - try { - await this.adapter.renameAsync(_adapter, oldName, newName, options); - } catch (error) { - if (error.message !== 'Not exists') { - throw error; - } - // else ignore, because folder cannot be deleted - } - } - - async _unlink(_adapter, name, options) { - // read if it is a file or folder - try { - // remove trailing '/' - if (name.endsWith('/')) { - name = name.substring(0, name.length - 1); - } - const files = await this.adapter.readDirAsync(_adapter, name, options); - if (files && files.length) { - for (let f = 0; f < files.length; f++) { - await this._unlink(_adapter, `${name}/${files[f].file}`); - } - } - } catch (error) { - // ignore, because it is a file and not a folder - if (error.message !== 'Not exists') { - throw error; - } - } - - try { - await this.adapter.unlinkAsync(_adapter, name, options); - } catch (error) { - if (error.message !== 'Not exists') { - throw error; - } - // else ignore, because folder cannot be deleted - } - } - - /** - * Convert errors into strings and then call cb - * @param {function} cb - callback - * @param {string|Error|null} error - error argument - * @param {any[]} args - args passed to cb - */ - static _fixCallback(cb, error, ...args) { - if (typeof cb !== 'function') { - return; - } - - if (error instanceof Error) { - error = error.message; - } - - cb(error, ...args); - } - - _checkPermissions(socket, command, callback, arg) { - if (socket._acl.user !== 'system.user.admin') { - // type: file, object, state, other - // operation: create, read, write, list, delete, sendto, execute, sendToHost, readLogs - if (SocketCommands.COMMANDS_PERMISSIONS[command]) { - // If permission required - if (SocketCommands.COMMANDS_PERMISSIONS[command].type) { - if (socket._acl[SocketCommands.COMMANDS_PERMISSIONS[command].type] && - socket._acl[SocketCommands.COMMANDS_PERMISSIONS[command].type][SocketCommands.COMMANDS_PERMISSIONS[command].operation]) { - return true; - } else { - this.adapter.log.warn(`No permission for "${socket._acl.user}" to call ${command}. Need "${SocketCommands.COMMANDS_PERMISSIONS[command].type}"."${SocketCommands.COMMANDS_PERMISSIONS[command].operation}"`); - } - } else { - return true; - } - } else { - this.adapter.log.warn(`No rule for command: ${command}`); - } - - if (typeof callback === 'function') { - callback(SocketCommands.ERROR_PERMISSION); - } else { - if (SocketCommands.COMMANDS_PERMISSIONS[command]) { - socket.emit(SocketCommands.ERROR_PERMISSION, { - command, - type: SocketCommands.COMMANDS_PERMISSIONS[command].type, - operation: SocketCommands.COMMANDS_PERMISSIONS[command].operation, - arg - }); - } else { - socket.emit(SocketCommands.ERROR_PERMISSION, {command, arg}); - } - } - return false; - } else { - return true; - } - } - - publish(socket, type, id, obj) { - if (socket && socket.subscribe && socket.subscribe[type] && this._updateSession(socket)) { - return !!socket.subscribe[type].find(sub => { - if (sub.regex.test(id)) { - // replace language - if (this.adapter._language && id === 'system.config' && obj.common) { - obj.common.language = this.adapter._language; - } - socket.emit(type, id, obj); - return true; - } - }); - } - - return false; - } - - publishFile(socket, id, fileName, size) { - if (socket && socket.subscribe && socket.subscribe.fileChange && this._updateSession(socket)) { - const key = `${id}####${fileName}`; - return !!socket.subscribe.fileChange.find(sub => { - if (sub.regex.test(key)) { - socket.emit('fileChange', id, fileName, size); - return true; - } - }); - } - - return false; - } - - publishInstanceMessage(socket, sourceInstance, messageType, data) { - if (this.clientSubscribes[socket.id] && - this.clientSubscribes[socket.id][sourceInstance] && - this.clientSubscribes[socket.id][sourceInstance].includes(messageType) - ) { - socket.emit('im', messageType, sourceInstance, data); - return true; - } - - // inform instance about missing subscription - this.adapter.sendTo(sourceInstance, 'clientSubscribeError', {type: messageType, sid: socket.id, reason: 'no one subscribed'}); - return false; - } - - _showSubscribes(socket, type) { - if (socket && socket.subscribe) { - const s = socket.subscribe[type] || []; - const ids = []; - for (let i = 0; i < s.length; i++) { - ids.push(s[i].pattern); - } - this.adapter.log.debug(`Subscribes: ${ids.join(', ')}`); - } else { - this.adapter.log.debug('Subscribes: no subscribes'); - } - } - - isLogEnabled() { - return this.logEnabled; - } - - subscribe(socket, type, pattern, patternFile) { - if (!pattern) { - return this.adapter.log.warn('Empty pattern on subscribe!'); - } - - this.subscribes[type] = this.subscribes[type] || {}; - - let p; - let key; - pattern = pattern.toString(); - if (patternFile && type === 'fileChange') { - patternFile = patternFile.toString(); - key = `${pattern}####${patternFile}`; - } else { - key = pattern; - } - - try { - p = pattern2RegEx(key); - } catch (e) { - this.adapter.log.error(`Invalid pattern on subscribe: ${e.message}`) - return - } - - if (p === null) { - return this.adapter.log.warn('Empty pattern on subscribe!'); - } - - let s; - if (socket) { - socket.subscribe = socket.subscribe || {}; - s = socket.subscribe[type] = socket.subscribe[type] || []; - - if (s.find(item => item.pattern === key)) { - return; - } - s.push({pattern: key, regex: new RegExp(p)}); - } - - const options = socket && socket._acl ? {user: socket._acl.user} : undefined; - - if (this.subscribes[type][key] === undefined) { - this.subscribes[type][key] = 1; - if (type === 'stateChange') { - this.adapter.subscribeForeignStates(pattern, options); - } else if (type === 'objectChange') { - this.adapter.subscribeForeignObjects && this.adapter.subscribeForeignObjects(pattern, options); - } else if (type === 'log') { - if (!this.logEnabled && this.adapter.requireLog) { - this.logEnabled = true; - this.adapter.requireLog(true, options); - } - } else if (type === 'fileChange' && this.adapter.subscribeForeignFiles) { - this.adapter.subscribeForeignFiles(pattern, patternFile, options); - } - } else { - this.subscribes[type][key]++; - } - }; - - unsubscribe(socket, type, pattern, patternFile) { - if (!pattern) { - return this.adapter.log.warn('Empty pattern on subscribe!'); - } - // console.log((socket._name || socket.id) + ' unsubscribe ' + pattern); - if (!this.subscribes[type]) { - return; - } - - let key; - pattern = pattern.toString(); - if (patternFile && type === 'fileChange') { - patternFile = patternFile.toString(); - key = `${pattern}####${patternFile}`; - } else { - key = pattern; - } - - const options = socket && socket._acl ? {user: socket._acl.user} : undefined; - - if (socket && typeof socket === 'object') { - if (!socket.subscribe || !socket.subscribe[type]) { - return; - } - - for (let i = socket.subscribe[type].length - 1; i >= 0; i--) { - if (socket.subscribe[type][i].pattern === key) { - // Remove pattern from global list - if (this.subscribes[type][key] !== undefined) { - this.subscribes[type][key]--; - if (this.subscribes[type][key] <= 0) { - if (type === 'stateChange') { - //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); - this.adapter.unsubscribeForeignStates(pattern, options); - } else if (type === 'objectChange') { - //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); - this.adapter.unsubscribeForeignObjects && this.adapter.unsubscribeForeignObjects(pattern, options); - } else if (type === 'log') { - //console.log((socket._name || socket.id) + ' requireLog false'); - if (this.logEnabled && this.adapter.requireLog) { - this.logEnabled = false; - this.adapter.requireLog(false, options); - } - } else if (type === 'fileChange') { - //console.log((socket._name || socket.id) + ' requireLog false'); - this.adapter.unsubscribeForeignFiles && this.adapter.unsubscribeForeignFiles(pattern, patternFile, options); - } - delete this.subscribes[type][pattern]; - } - } - - delete socket.subscribe[type][i]; - socket.subscribe[type].splice(i, 1); - return; - } - } - } else if (key) { - // Remove a pattern from a global list - if (this.subscribes[type][key] !== undefined) { - this.subscribes[type][key]--; - if (this.subscribes[type][key] <= 0) { - if (type === 'stateChange') { - this.adapter.unsubscribeForeignStates(pattern, options); - } else if (type === 'objectChange') { - this.adapter.unsubscribeForeignObjects && this.adapter.unsubscribeForeignObjects(pattern, options); - } else if (type === 'log') { - if (this.adapter.requireLog && this.logEnabled) { - this.logEnabled = false; - this.adapter.requireLog(false, options); - } - } else if (type === 'fileChange') { - this.adapter.unsubscribeForeignFiles && this.adapter.unsubscribeForeignFiles(pattern, patternFile, options); - } - delete this.subscribes[type][key]; - } - } - } else { - Object.keys(this.subscribes[type]).forEach(pattern => { - if (type === 'stateChange') { - //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); - this.adapter.unsubscribeForeignStates(pattern, options); - } else if (type === 'objectChange') { - //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); - this.adapter.unsubscribeForeignObjects && this.adapter.unsubscribeForeignObjects(pattern, options); - } else if (type === 'log') { - //console.log((socket._name || socket.id) + ' requireLog false'); - if (this.adapter.requireLog && this.logEnabled) { - this.logEnabled = false; - this.adapter.requireLog(false, options); - } - } else if (type === 'fileChange') { - const [id, fileName] = pattern.split('####'); - this.adapter.unsubscribeForeignFiles && this.adapter.unsubscribeForeignFiles(id, fileName, options); - } - }); - - this.subscribes[type] = {} - } - }; - - subscribeSocket(socket, type) { - if (!socket || !socket.subscribe) { - return; - } - - if (!type) { - // all - return Object.keys(socket.subscribe) - .forEach(type => this.subscribeSocket(socket, type)); - } - - if (!socket.subscribe[type]) { - return; - } - - const options = socket && socket._acl ? {user: socket._acl.user} : undefined; - - for (let i = 0; i < socket.subscribe[type].length; i++) { - const pattern = socket.subscribe[type][i].pattern; - if (this.subscribes[type][pattern] === undefined) { - this.subscribes[type][pattern] = 1; - if (type === 'stateChange') { - this.adapter.subscribeForeignStates(pattern, options); - } else if (type === 'objectChange') { - this.adapter.subscribeForeignObjects && this.adapter.subscribeForeignObjects(pattern, options); - } else if (type === 'log') { - if (this.adapter.requireLog && !this.logEnabled) { - this.logEnabled = true; - this.adapter.requireLog(true, options); - } - } else if (type === 'fileChange') { - const [id, fileName] = pattern.split('####'); - this.adapter.subscribeForeignFiles && this.adapter.subscribeForeignFiles(id, fileName, options); - } - } else { - this.subscribes[type][pattern]++; - } - } - } - - unsubscribeSocket(socket, type) { - if (!socket || !socket.subscribe) { - return; - } - // inform all instances about disconnected socket - this._informAboutDisconnect(socket.id); - - if (!type) { - // all - return Object.keys(socket.subscribe) - .forEach(type => this.unsubscribeSocket(socket, type)); - } - - if (!socket.subscribe[type]) { - return; - } - - const options = socket && socket._acl ? {user: socket._acl.user} : undefined; - - for (let i = 0; i < socket.subscribe[type].length; i++) { - const pattern = socket.subscribe[type][i].pattern; - if (this.subscribes[type][pattern] !== undefined) { - this.subscribes[type][pattern]--; - if (this.subscribes[type][pattern] <= 0) { - if (type === 'stateChange') { - this.adapter.unsubscribeForeignStates(pattern, options); - } else if (type === 'objectChange') { - this.adapter.unsubscribeForeignObjects && this.adapter.unsubscribeForeignObjects(pattern, options); - } else if (type === 'log') { - if (this.adapter.requireLog && !this.logEnabled) { - this.logEnabled = true; - this.adapter.requireLog(true, options); - } - } else if (type === 'fileChange') { - const [id, fileName] = pattern.split('####'); - this.adapter.unsubscribeForeignFiles && this.adapter.unsubscribeForeignFiles(id, fileName, options); - } - delete this.subscribes[type][pattern]; - } - } - } - } - - _subscribeStates(socket, pattern, callback) { - if (this._checkPermissions(socket, 'subscribe', callback, pattern)) { - if (Array.isArray(pattern)) { - for (let p = 0; p < pattern.length; p++) { - this.subscribe(socket, 'stateChange', pattern[p]); - } - } else { - this.subscribe(socket, 'stateChange', pattern); - } - - this.adapter.log.level === 'debug' && this._showSubscribes(socket, 'stateChange'); - - typeof callback === 'function' && setImmediate(callback, null); - } - } - - _unsubscribeStates(socket, pattern, callback) { - if (this._checkPermissions(socket, 'unsubscribe', callback, pattern)) { - if (pattern && typeof pattern === 'object' && pattern instanceof Array) { - for (let p = 0; p < pattern.length; p++) { - this.unsubscribe(socket, 'stateChange', pattern[p]); - } - } else { - this.unsubscribe(socket, 'stateChange', pattern); - } - - this.adapter.log.level === 'debug' && this._showSubscribes(socket, 'stateChange'); - - typeof callback === 'function' && setImmediate(callback, null); - } - } - - _subscribeFiles(socket, id, pattern, callback) { - if (this._checkPermissions(socket, 'subscribeFiles', callback, pattern)) { - if (Array.isArray(pattern)) { - for (let p = 0; p < pattern.length; p++) { - this.subscribe(socket, 'fileChange', id, pattern[p]); - } - } else { - this.subscribe(socket, 'fileChange', id, pattern); - } - - this.adapter.log.level === 'debug' && this._showSubscribes(socket, 'fileChange'); - - typeof callback === 'function' && setImmediate(callback, null); - } - } - - _unsubscribeFiles(socket, id, pattern, callback) { - if (this._checkPermissions(socket, 'unsubscribeFiles', callback, pattern)) { - if (Array.isArray(pattern)) { - for (let p = 0; p < pattern.length; p++) { - this.unsubscribe(socket, 'fileChange', id, pattern[p]); - } - } else { - this.unsubscribe(socket, 'fileChange', id, pattern); - } - - this.adapter.log.level === 'debug' && this._showSubscribes(socket, 'fileChange'); - - typeof callback === 'function' && setImmediate(callback, null); - } - } - - addCommandHandler(command, handler) { - if (handler) { - this.commands[command] = handler; - } else if (this.commands.hasOwnProperty(command)) { - delete this.commands[command]; - } - } - - getCommandHandler(command) { - return this.commands[command]; - } - - _fixAdminUI(obj) { - if (obj && obj.common && !obj.common.adminUI) { - if (obj.common.noConfig) { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.config = 'none'; - } else if (obj.common.jsonConfig) { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.config = 'json'; - } else if (obj.common.materialize) { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.config = 'materialize'; - } else { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.config = 'html'; - } - - if (obj.common.jsonCustom) { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.custom = 'json'; - } else if (obj.common.supportCustoms) { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.custom = 'json'; - } - - if (obj.common.materializeTab && obj.common.adminTab) { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.tab = 'materialize'; - } else if (obj.common.adminTab) { - obj.common.adminUI = obj.common.adminUI || {}; - obj.common.adminUI.tab = 'html'; - } - - obj.common.adminUI && this.adapter.log.debug(`Please add to "${obj._id.replace(/\.\d+$/, '')}" common.adminUI=${JSON.stringify(obj.common.adminUI)}`); - } - } - - __initCommandsCommon() { - this.commands['authenticate'] = (socket, user, pass, callback) => { - // Authenticate user by login and password - // @param {string} user - user name - // @param {string} pass - password - // @param {function} callback - `function (isUserAuthenticated, isAuthenticationUsed)` - if (socket && socket.___socket) { - socket = socket.___socket; - } - - this.adapter.log.debug(`${new Date().toISOString()} Request authenticate [${socket._acl.user}]`); - if (typeof user === 'function') { - callback = user; - // user = undefined; - } - if (socket._acl.user !== null) { - if (typeof callback === 'function') { - callback(true, socket._secure); - } - } else { - this.adapter.log.debug(`${new Date().toISOString()} Request authenticate [${socket._acl.user}]`); - socket._authPending = callback; - } - }; - - this.commands['error'] = (socket, error) => { - // Write error into ioBroker log - // @param {string} error - error text - this.adapter.log.error(`Socket error: ${error}`); - }; - - this.commands['log'] = (socket, text, level) => { - // Write log entry into ioBroker log - // @param {string} text - log text - // @param {string} level - one of `['silly', 'debug', 'info', 'warn', 'error']`. Default is 'debug'. - if (level === 'error') { - this.adapter.log.error(text); - } else if (level === 'warn') { - this.adapter.log.warn(text); - } else if (level === 'info') { - this.adapter.log.info(text); - } else { - this.adapter.log.debug(text); - } - }; - - this.commands['checkFeatureSupported'] = (socket, feature, callback) => { - // Checks, if the same feature is supported by the current js-controller - // @param {string} feature - feature name like `CONTROLLER_LICENSE_MANAGER` - // @param {function} callback - `function (error, isSupported)` - if (feature === 'INSTANCE_MESSAGES') { - SocketCommands._fixCallback(callback, null, true); - } else { - SocketCommands._fixCallback(callback, null, this.adapter.supportsFeature && this.adapter.supportsFeature(feature)); - } - }; - - // new History - this.commands['getHistory'] = (socket, id, options, callback) => { - // Get history data from specific instance - // @param {string} id - object ID - // @param {object} options - See object description here: https://github.com/ioBroker/ioBroker.history/blob/master/docs/en/README.md#access-values-from-javascript-adapter - // @param {function} callback - `function (error, result)` - if (this._checkPermissions(socket, 'getStateHistory', callback, id)) { - if (typeof options === 'string') { - options = { - instance: options - }; - } - options = options || {}; - options.user = socket._acl.user; - options.aggregate = options.aggregate || 'none'; - try { - this.adapter.getHistory(id, options, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[getHistory] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - // HTTP - this.commands['httpGet'] = (socket, url, callback) => { - // Read content of HTTP(S) page server-side (without CORS and stuff) - // @param {string} url - Page URL - // @param {function} callback - `function (error, {status, statusText}, body)` - if (this._checkPermissions(socket, 'httpGet', callback, url)) { - axios = axios || require('axios'); - this.adapter.log.debug(`httpGet: ${url}`); - try { - axios(url, { - responseType: 'arraybuffer', - timeout: 15000, - validateStatus: status => status < 400 - }) - .then(result => callback(null, {status: result.status, statusText: result.statusText}, result.data)) - .catch(error => callback(error)); - } catch (error) { - callback(error); - } - } - }; - - // commands - this.commands['sendTo'] = (socket, adapterInstance, command, message, callback) => { - // Send the message to specific instance - // @param {string} adapterInstance - instance name, e.g. `history.0` - // @param {string} command - command name - // @param {object} message - the message is instance dependent - // @param {function} callback - `function (result)` - if (this._checkPermissions(socket, 'sendTo', callback, command)) { - try { - this.adapter.sendTo(adapterInstance, command, message, res => - typeof callback === 'function' && setImmediate(() => - callback(res))); - } catch (error) { - typeof callback === 'function' && setImmediate(() => callback({error})); - } - } - }; - - // following commands are protected and require the extra permissions - const protectedCommands = ['cmdExec', 'getLocationOnDisk', 'getDiagData', 'getDevList', 'delLogs', 'writeDirAsZip', 'writeObjectsAsZip', 'readObjectsAsZip', 'checkLogging', 'updateMultihost', 'rebuildAdapter']; - - this.commands['sendToHost'] = (socket, host, command, message, callback) => { - // Send a message to the specific host. - // Host can answer to the following commands: `cmdExec, getRepository, getInstalled, getInstalledAdapter, getVersion, getDiagData, getLocationOnDisk, getDevList, getLogs, getHostInfo, delLogs, readDirAsZip, writeDirAsZip, readObjectsAsZip, writeObjectsAsZip, checkLogging, updateMultihost`. - // @param {string} host - instance name, e.g. `history.0` - // @param {string} command - command name - // @param {object} message - the message is command-specific - // @param {function} callback - `function (result)` - if (this._checkPermissions(socket, protectedCommands.includes(command) ? 'cmdExec' : 'sendToHost', callback, command)) { - // Try to decode this file locally as redis has a limitation for files bigger than 20MB - if (command === 'writeDirAsZip' && message && message.data.length > 1024 * 1024) { - let buffer; - try { - buffer = Buffer.from(message.data, 'base64'); - } catch (error) { - this.adapter.log.error(`Cannot convert data: ${error.toString()}`); - return callback && callback({error: `Cannot convert data: ${error.toString()}`}); - } - - zipFiles = zipFiles || utils.commonTools.zipFiles; - - zipFiles - .writeDirAsZip( - this.adapter, // normally we have to pass here the internal "objects" object, but as - // only writeFile is used, and it has the same name, we can pass here the - // adapter, which has the function with the same name and arguments - message.id, - message.name, - buffer, - message.options, - error => callback({ error }) // this is for back compatibility with js-controller@4.0 or older - ) - .then(() => callback({})) - .catch(error => { - this.adapter.log.error(`Cannot write zip file as folder: ${error.toString()}`); - callback && callback({ error }); - }); - } else if (this._sendToHost) { - this._sendToHost(host, command, message, callback); - } else { - try { - this.adapter.sendToHost(host, command, message, callback); - } catch (error) { - return callback && callback({ error }); - } - } - } - }; - - this.commands['authEnabled'] = (socket, callback) => { - // Ask server is authentication enabled, and if the user authenticated - // @param {function} callback - `function (isAuthenticationUsed, userName)` - if (this._checkPermissions(socket, 'authEnabled', callback)) { - if (typeof callback === 'function') { - callback(this.adapter.config.auth, (socket._acl.user || '').replace(/^system\.user\./, '')); - } else { - this.adapter.log.warn('[authEnabled] Invalid callback'); - } - } - }; - - this.commands['logout'] = (socket, callback) => { - // Logout user - // @param {function} callback - function (error) - this.adapter.destroySession(socket._sessionID, callback); - }; - - this.commands['listPermissions'] = (socket, callback) => { - // List commands and permissions - // @param {function} callback - `function (permissions)` - if (typeof callback === 'function') { - callback(SocketCommands.COMMANDS_PERMISSIONS); - } else { - this.adapter.log.warn('[listPermissions] Invalid callback'); - } - }; - - this.commands['getUserPermissions'] = (socket, callback) => { - // Get user permissions - // @param {function} callback - `function (error, permissions)` - if (this._checkPermissions(socket, 'getUserPermissions', callback)) { - if (typeof callback === 'function') { - callback(null, socket._acl); - } else { - this.adapter.log.warn('[getUserPermissions] Invalid callback'); - } - } - }; - - this.commands['getVersion'] = (socket, callback) => { - // Get the adapter version. Not the socket-classes version! - // @param {function} callback - `function (error, adapterVersion, adapterName)` - if (this._checkPermissions(socket, 'getVersion', callback)) { - if (typeof callback === 'function') { - callback(null, this.adapter.version, this.adapter.name); - } else { - this.adapter.log.warn('[getVersion] Invalid callback'); - } - } - }; - - this.commands['getAdapterName'] = (socket, callback) => { - // Get adapter name. Not the socket-classes version! - // @param {function} callback - `function (error, adapterVersion)` - if (this._checkPermissions(socket, 'getAdapterName', callback)) { - if (typeof callback === 'function') { - callback(null, this.adapter.name || 'unknown'); - } else { - this.adapter.log.warn('[getAdapterName] Invalid callback'); - } - } - }; - } - - __initCommandsFiles() { - // file operations - this.commands['readFile'] = (socket, _adapter, fileName, callback) => { - // Read file from ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} fileName - file name, e.g. `main/vis-views.json` - // @param {function} callback - `function (error, data, mimeType)` - if (this._checkPermissions(socket, 'readFile', callback, fileName)) { - try { - this.adapter.readFile(_adapter, fileName, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[readFile] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['readFile64'] = (socket, _adapter, fileName, callback) => { - // Read file from ioBroker DB as base64 string - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} fileName - file name, e.g. `main/vis-views.json` - // @param {function} callback - `function (error, base64, mimeType)` - if (this._checkPermissions(socket, 'readFile64', callback, fileName)) { - try { - this.adapter.readFile(_adapter, fileName, {user: socket._acl.user}, (error, buffer, type) => { - let data64; - if (buffer) { - try { - if (type === 'application/json' || type === 'application/json5' || fileName.toLowerCase().endsWith('.json5')) { - data64 = Buffer.from(encodeURIComponent(buffer)).toString('base64'); - } else { - if (typeof buffer === 'string') { - data64 = Buffer.from(buffer).toString('base64'); - } else { - data64 = buffer.toString('base64'); - } - } - } catch (error) { - this.adapter.log.error(`[readFile64] Cannot convert data: ${error.toString()}`); - } - } - - //Convert buffer to base 64 - if (typeof callback === 'function') { - callback(error, data64 || '', type); - } else { - this.adapter.log.warn('[readFile64] Invalid callback'); - } - }); - } catch (error) { - this.adapter.log.error(`[readFile64] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['writeFile64'] = (socket, _adapter, fileName, data64, options, callback) => { - // Write file into ioBroker DB as base64 string - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} fileName - file name, e.g. `main/vis-views.json` - // @param {string} data64 - file content as base64 string - // @param {object} options - optional `{mode: 0x0644}` - // @param {function} callback - `function (error)` - if (typeof options === 'function') { - callback = options; - options = {user: socket._acl.user}; - } - - options = options || {}; - options.user = socket._acl.user; - - if (this._checkPermissions(socket, 'writeFile64', callback, fileName)) { - if (!data64) { - return SocketCommands._fixCallback(callback, 'No data provided'); - } - // Convert base 64 to buffer - - try { - const buffer = Buffer.from(data64, 'base64'); - this.adapter.writeFile(_adapter, fileName, buffer, options, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[writeFile64] Cannot convert data: ${error.toString()}`); - SocketCommands._fixCallback(callback, `Cannot convert data: ${error.toString()}`); - } - } - }; - - // this function is overloaded in admin (because admin accepts only base64) - this.commands['writeFile'] = (socket, _adapter, fileName, data, options, callback) => { - // Write file into ioBroker DB as text **DEPRECATED** - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} fileName - file name, e.g. `main/vis-views.json` - // @param {string} data64 - file content as base64 string - // @param {object} options - optional `{mode: 0x644}` - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'writeFile', callback, fileName)) { - if (typeof options === 'function') { - callback = options; - options = {user: socket._acl.user}; - } - options = options || {}; - options.user = socket._acl.user; - this.adapter.log.debug('writeFile deprecated. Please use writeFile64'); - // const buffer = Buffer.from(data64, 'base64'); - try { - this.adapter.writeFile(_adapter, fileName, data, options, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[writeFile] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['unlink'] = (socket, _adapter, name, callback) => { - // Delete file in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} name - file name, e.g. `main/vis-views.json` - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'unlink', callback, name)) { - try { - this._unlink(_adapter, name, {user: socket._acl.user}) - .then(() => SocketCommands._fixCallback(callback)) - .catch(error => SocketCommands._fixCallback(callback, error)); - } catch (error) { - this.adapter.log.error(`[unlink] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['deleteFile'] = (socket, _adapter, name, callback) => { - // Delete file in ioBroker DB (same as unlink, but only for files) - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} name - file name, e.g. `main/vis-views.json` - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'unlink', callback, name)) { - try { - this.adapter.unlink(_adapter, name, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[deleteFile] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['deleteFolder'] = (socket, _adapter, name, callback) => { - // Delete file in ioBroker DB (same as unlink, but only for folders) - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} name - folder name, e.g. `main` - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'unlink', callback, name)) { - try { - this._unlink(_adapter, name, {user: socket._acl.user}) - .then(() => SocketCommands._fixCallback(callback, null)) - .catch(error => SocketCommands._fixCallback(callback, error)); - } catch (error) { - this.adapter.log.error(`[deleteFolder] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['renameFile'] = (socket, _adapter, oldName, newName, callback) => { - // Rename file in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} oldName - current file name, e.g. `main/vis-views.json` - // @param {string} newName - new file name, e.g. `main/vis-views-new.json` - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'rename', callback, oldName)) { - try { - this.adapter.rename(_adapter, oldName, newName, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[renameFile] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['rename'] = (socket, _adapter, oldName, newName, callback) => { - // Rename file or folder in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} oldName - current file name, e.g. `main/vis-views.json` - // @param {string} newName - new file name, e.g. `main/vis-views-new.json` - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'rename', callback, oldName)) { - try { - this._rename(_adapter, oldName, newName, {user: socket._acl.user}) - .then(() => SocketCommands._fixCallback(callback)) - .catch(error => SocketCommands._fixCallback(callback, error)); - } catch (error) { - this.adapter.log.error(`[rename] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['mkdir'] = (socket, _adapter, dirName, callback) => { - // Create folder in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} dirName - desired folder name, e.g. `main` - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'mkdir', callback, dirName)) { - try { - this.adapter.mkdir(_adapter, dirName, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[mkdir] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['readDir'] = (socket, _adapter, dirName, options, callback) => { - // Read content of folder in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} dirName - folder name, e.g. `main` - // @param {object} options - optional `{filter: '*'}` or `{filter: '*.json'}` - // @param {function} callback - `function (error, files)` where `files` is an array of objects, like `{file: 'vis-views.json', isDir: false, stats: {size: 123}, modifiedAt: 1661336290090, acl: {owner: 'system.user.admin', ownerGroup: 'system.group.administrator', permissions: 1632, read: true, write: true}` - if (typeof options === 'function') { - callback = options; - options = {}; - } - options = options || {}; - options.user = socket._acl.user; - - if (options.filter === undefined) { - options.filter = true; - } - - if (this._checkPermissions(socket, 'readDir', callback, dirName)) { - try { - this.adapter.readDir(_adapter, dirName, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[readDir] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['chmodFile'] = (socket, _adapter, fileName, options, callback) => { - // Change file mode in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} fileName - file name, e.g. `main/vis-views.json` - // @param {object} options - `{mode: 0x644}` or 0x644. The first digit is user, second group, third others. Bit 1 is `execute`, bit 2 is `write`, bit 3 is `read` - // @param {function} callback - `function (error)` - if (typeof options === 'function') { - callback = options; - options = {}; - } - options = options || {}; - options.user = socket._acl.user; - - if (options.filter === undefined) { - options.filter = true; - } - - if (this._checkPermissions(socket, 'chmodFile', callback, fileName)) { - try { - this.adapter.chmodFile(_adapter, fileName, options, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[chmodFile] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['chownFile'] = (socket, _adapter, fileName, options, callback) => { - // Change file owner in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} fileName - file name, e.g. `main/vis-views.json` - // @param {object} options - `{owner: 'system.user.user', ownerGroup: ''system.group.administrator'}` or 'system.user.user'. If ownerGroup is not defined, it will be taken from owner. - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'chownFile', callback, fileName)) { - options = options || {}; - options.user = socket._acl.user; - try { - this.adapter.chownFile(_adapter, fileName, options, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[chownFile] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['fileExists'] = (socket, _adapter, fileName, callback) => { - // Check if the file or folder exists in ioBroker DB - // @param {string} _adapter - instance name, e.g. `vis.0` - // @param {string} fileName - file name, e.g. `main/vis-views.json` - // @param {function} callback - `function (error, isExist)` - if (this._checkPermissions(socket, 'fileExists', callback, fileName)) { - try { - this.adapter.fileExists(_adapter, fileName, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[fileExists] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['subscribeFiles'] = (socket, id, pattern, callback) => { - // Subscribe to file changes in ioBroker DB - // @param {string} id - instance name, e.g. `vis.0` or any object ID of type `meta`. `id` could have wildcards `*` too. - // @param {string} pattern - file name pattern, e.g. `main/*.json` - // @param {function} callback - `function (error)` - return this._subscribeFiles(socket, id, pattern, callback); - } - - this.commands['unsubscribeFiles'] = (socket, id, pattern, callback) => { - // Unsubscribe from file changes in ioBroker DB - // @param {string} id - instance name, e.g. `vis.0` or any object ID of type `meta`. `id` could have wildcards `*` too. - // @param {string} pattern - file name pattern, e.g. `main/*.json` - // @param {function} callback - `function (error)` - - return this._unsubscribeFiles(socket, id, pattern, callback); - } - - this.commands['getAdapterInstances'] = (socket, adapterName, callback) => { - // Read all instances of the given adapter, or all instances of all adapters if adapterName is not defined - // @param {string} adapterName - optional adapter name, e.g. `history`. - // @param {function} callback - `function (error, instanceList)`, where instanceList is an array of instance objects, e.g. `{_id: 'system.adapter.history.0', common: {name: 'history', ...}, native: {...}}` - if (typeof callback === 'function') { - if (this._checkPermissions(socket, 'getObject', callback)) { - let _adapterName = adapterName !== undefined && adapterName !== null ? adapterName : this.adapterName || ''; - if (_adapterName) { - _adapterName += '.'; - } - try { - this.adapter.getObjectView('system', 'instance', - {startkey: `system.adapter.${_adapterName}`, endkey: `system.adapter.${_adapterName}\u9999`}, - {user: socket._acl.user}, - (error, doc) => { - if (error) { - callback(error); - } else { - callback(null, doc.rows - .map(item => { - const obj = item.value; - if (obj.common) { - delete obj.common.news; - } - this._fixAdminUI(obj); - return obj; - }) - .filter(obj => obj && (!adapterName || (obj.common && obj.common.name === adapterName)))); - } - }); - - } catch (error) { - this.adapter.log.error(`[getAdapterInstances] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - } - }; - } - - __initCommandsStates() { - this.commands['getStates'] = (socket, pattern, callback) => { - // Read states by pattern - // @param {string} pattern - optional pattern, like `system.adapter.*` or array of state IDs - // @param {function} callback - `function (error, states)`, where `states` is an object like `{'system.adapter.history.0': {_id: 'system.adapter.history.0', common: {name: 'history', ...}, native: {...}, 'system.adapter.history.1': {...}}}` - if (this._checkPermissions(socket, 'getStates', callback, pattern)) { - if (typeof pattern === 'function') { - callback = pattern; - pattern = null; - } - if (typeof callback === 'function') { - try { - this.adapter.getForeignStates(pattern || '*', {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[getStates] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } else { - this.adapter.log.warn('[getStates] Invalid callback') - } - } - }; - - this.commands['getForeignStates'] = (socket, pattern, callback) => { - // Read all states (which might not belong to this adapter) which match the given pattern - // @param {string} pattern - pattern like `system.adapter.*` or array of state IDs - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'getStates', callback)) { - if (typeof callback === 'function') { - try { - this.adapter.getForeignStates(pattern, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[getForeignStates] ERROR: ${error}`); - SocketCommands._fixCallback(callback, error); - } - } else { - this.adapter.log.warn('[getForeignStates] Invalid callback') - } - } - }; - - this.commands['getState'] = (socket, id, callback) => { - // Read one state. - // @param {string} id - State ID like, 'system.adapter.admin.0.memRss' - // @param {function} callback - `function (error, state)`, where `state` is an object like `{val: 123, ts: 1663915537418, ack: true, from: 'system.adapter.admin.0', q: 0, lc: 1663915537418, c: 'javascript.0'}` - if (this._checkPermissions(socket, 'getState', callback, id)) { - if (typeof callback === 'function') { - if (this.states && this.states[id]) { - callback(null, this.states[id]); - } else { - try { - this.adapter.getForeignState(id, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[getState] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - } else { - this.adapter.log.warn('[getState] Invalid callback'); - } - } - }; - - this.commands['setState'] = (socket, id, state, callback) => { - // Write one state. - // @param {string} id - State ID like, 'system.adapter.admin.0.memRss' - // @param {any} state - value or object like `{val: 123, ack: true}` - // @param {function} callback - `function (error, state)`, where `state` is an object like `{val: 123, ts: 1663915537418, ack: true, from: 'system.adapter.admin.0', q: 0, lc: 1663915537418, c: 'javascript.0'}` - if (this._checkPermissions(socket, 'setState', callback, id)) { - if (typeof state !== 'object') { - state = {val: state}; - } - - // clear cache - if (this.states && this.states[id]) { - delete this.states[id]; - } - - try { - this.adapter.setForeignState(id, state, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[setState] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['getBinaryState'] = (socket, id, callback) => { - // Read one binary state. - // @param {string} id - State ID like, 'javascript.0.binary' - // @param {function} callback - `function (error, base64)` - if (this._checkPermissions(socket, 'getState', callback, id)) { - if (typeof callback === 'function') { - try { - if (this.adapter.getForeignBinaryState) { - this.adapter.getForeignBinaryState(id, {user: socket._acl.user}, (error, data) => { - if (data) { - try { - data = Buffer.from(data).toString('base64'); - } catch (error) { - this.adapter.log.error(`[getBinaryState] Cannot convert data: ${error.toString()}`); - } - } - SocketCommands._fixCallback(callback, error, data); - }); - } else { - this.adapter.getBinaryState(id, {user: socket._acl.user}, (error, data) => { - if (data) { - try { - data = Buffer.from(data).toString('base64'); - } catch (error) { - this.adapter.log.error(`[getBinaryState] Cannot convert data: ${error.toString()}`); - } - } - SocketCommands._fixCallback(callback, error, data); - }); - } - } catch (error) { - this.adapter.log.error(`[getBinaryState] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } else { - this.adapter.log.warn('[getBinaryState] Invalid callback') - } - } - }; - - this.commands['setBinaryState'] = (socket, id, base64, callback) => { - // Write one binary state. - // @param {string} id - State ID like, 'javascript.0.binary' - // @param {string} base64 - State value as base64 string. Binary states have no acknowledged flag. - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'setState', callback, id)) { - if (typeof callback === 'function') { - let data = null; - try { - data = Buffer.from(base64, 'base64') - } catch (error) { - this.adapter.log.warn(`[setBinaryState] Cannot convert base64 data: ${error.toString()}`); - } - - try { - if (this.adapter.setForeignBinaryState) { - this.adapter.setForeignBinaryState(id, data, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } else { - this.adapter.setBinaryState(id, data, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } - } catch (error) { - this.adapter.log.error(`[setBinaryState] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } else { - this.adapter.log.warn('[setBinaryState] Invalid callback'); - } - } - }; - - this.commands['subscribe'] = (socket, pattern, callback) => { - // Subscribe to state changes by pattern. The events will come as 'stateChange' events to the socket. - // @param {string} pattern - pattern like 'system.adapter.*' or array of states like ['system.adapter.admin.0.memRss', 'system.adapter.admin.0.memHeapTotal'] - // @param {function} callback - `function (error)` - return this._subscribeStates(socket, pattern, callback); - }; - - this.commands['subscribeStates'] = (socket, pattern, callback) => { - // Subscribe to state changes by pattern. Same as `subscribe`. The events will come as 'stateChange' events to the socket. - // @param {string} pattern - pattern like 'system.adapter.*' or array of states like ['system.adapter.admin.0.memRss', 'system.adapter.admin.0.memHeapTotal'] - // @param {function} callback - `function (error)` - return this._subscribeStates(socket, pattern, callback); - }; - - this.commands['unsubscribe'] = (socket, pattern, callback) => { - // Unsubscribe from state changes by pattern. - // @param {string} pattern - pattern like 'system.adapter.*' or array of states like ['system.adapter.admin.0.memRss', 'system.adapter.admin.0.memHeapTotal'] - // @param {function} callback - `function (error)` - return this._unsubscribeStates(socket, pattern, callback); - }; - - this.commands['unsubscribeStates'] = (socket, pattern, callback) => { - // Unsubscribe from state changes by pattern. Same as `unsubscribe`. - // @param {string} pattern - pattern like 'system.adapter.*' or array of states like ['system.adapter.admin.0.memRss', 'system.adapter.admin.0.memHeapTotal'] - // @param {function} callback - `function (error)` - return this._unsubscribeStates(socket, pattern, callback); - }; - } - - __initCommandsObjects() { - this.commands['getObject'] = (socket, id, callback) => { - // Get one object - // @param {string} id - object ID. - // @param {function} callback - `function (error, obj)` - if (this._checkPermissions(socket, 'getObject', callback, id)) { - try { - this.adapter.getForeignObject(id, {user: socket._acl.user}, (error, obj) => { - // overload language from current instance - if (this.adapter._language && id === 'system.config' && obj.common) { - obj.common.language = this.adapter._language; - } - SocketCommands._fixCallback(callback, error, obj); - }); - } catch (error) { - this.adapter.log.error(`[getObject] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - // not admin version of "all objects" - // this function is overloaded in admin - this.commands['getObjects'] = (socket, list, callback) => { - // Get all objects that are relevant for web: all states and enums with rooms - // @param {string} id - object ID. - // @param {string[]} list - optional list of IDs. - // @param {function} callback - `function (error, obj)` - if (typeof list === 'function') { - callback = list; - list = null; - } - if (list && list.length) { - if (this._checkPermissions(socket, 'getObject', callback)) { - if (typeof callback === 'function') { - try { - this.adapter.getForeignObjects(list, {user: socket._acl.user}, (error, objs) => - SocketCommands._fixCallback(callback, error, objs)); - } catch (error) { - this.adapter.log.error(`[getObjects] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } else { - this.adapter.log.warn('[getObjects] Invalid callback'); - } - } - } else if (this._checkPermissions(socket, 'getObjects', callback)) { - try { - if (typeof callback === 'function') { - this.adapter.getForeignObjects('*', 'state', 'rooms', {user: socket._acl.user}, async (error, objs) => { - try { - const channels = await this.adapter.getForeignObjectsAsync('*', 'channel', null, {user: socket._acl.user}); - const devices = await this.adapter.getForeignObjectsAsync('*', 'device', null, {user: socket._acl.user}); - const enums = await this.adapter.getForeignObjectsAsync('*', 'enum', null, {user: socket._acl.user}); - const config = await this.adapter.getForeignObjectAsync('system.config', {user: socket._acl.user}); - Object.assign(objs, channels, devices, enums); - objs['system.config'] = config; - } catch (e) { - this.adapter.log.error(`[getObjects] ERROR: ${e.toString()}`); - } - // overload language - if (this.adapter._language && objs['system.config'] && objs['system.config'].common) { - objs['system.config'].common.language = this.adapter._language; - } - - SocketCommands._fixCallback(callback, error, objs); - }); - } else { - this.adapter.log.warn('[getObjects] Invalid callback'); - } - } catch (error) { - this.adapter.log.error(`[getObjects] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - this.commands['subscribeObjects'] = (socket, pattern, callback) => { - // Subscribe to object changes by pattern. The events will come as 'objectChange' events to the socket. - // @param {string} pattern - pattern like 'system.adapter.*' or array of IDs like ['system.adapter.admin.0.memRss', 'system.adapter.admin.0.memHeapTotal'] - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'subscribeObjects', callback, pattern)) { - try { - if (pattern && typeof pattern === 'object' && pattern instanceof Array) { - for (let p = 0; p < pattern.length; p++) { - this.subscribe(socket, 'objectChange', pattern[p]); - } - } else { - this.subscribe(socket, 'objectChange', pattern); - } - if (typeof callback === 'function') { - setImmediate(callback, null); - } - } catch (error) { - if (typeof callback === 'function') { - setImmediate(callback, error); - } - } - } - }; - - this.commands['unsubscribeObjects'] = (socket, pattern, callback) => { - // Unsubscribe from object changes by pattern. - // @param {string} pattern - pattern like 'system.adapter.*' or array of IDs like ['system.adapter.admin.0.memRss', 'system.adapter.admin.0.memHeapTotal'] - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'unsubscribeObjects', callback, pattern)) { - try { - if (pattern && typeof pattern === 'object' && pattern instanceof Array) { - for (let p = 0; p < pattern.length; p++) { - this.unsubscribe(socket, 'objectChange', pattern[p]); - } - } else { - this.unsubscribe(socket, 'objectChange', pattern); - } - if (typeof callback === 'function') { - setImmediate(callback, null); - } - } catch (error) { - if (typeof callback === 'function') { - setImmediate(callback, error); - } - } - } - }; - - this.commands['getObjectView'] = (socket, design, search, params, callback) => { - // Make a query to the object database. - // @param {string} design - 'system' or other designs like `custom`, but it must exist object `_design/custom`. Too 99,9% use `system`. - // @param {string} search - object type, like `state`, `instance`, `adapter`, `host`, ... - // @param {string} params - parameters for the query in form `{startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}` - // @param {function} callback - `function (error)` - if (typeof callback === 'function') { - if (this._checkPermissions(socket, 'getObjectView', callback, search)) { - try { - this.adapter.getObjectView(design, search, params, {user: socket._acl.user}, callback); - } catch (error) { - this.adapter.log.error(`[getObjectView] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - } else { - this.adapter.log.error('Callback is not a function'); - } - }; - - this.commands['setObject'] = (socket, id, obj, callback) => { - // Set object. - // @param {string} id - object ID - // @param {object} obj - object itself - // @param {function} callback - `function (error)` - if (this._checkPermissions(socket, 'setObject', callback, id)) { - try { - this.adapter.setForeignObject(id, obj, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[setObject] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - }; - - // this function is overloaded in admin - this.commands['delObject'] = (socket, id, options, callback) => { - // Delete object. Only deletion of flot objects is allowed - // @param {string} id - Object ID like, 'flot.0.myChart' - // @param {string} options - ignored - // @param {function} callback - `function (error)` - if (id.startsWith('flot.') || id.startsWith('fullcalendar.')) { - if (this._checkPermissions(socket, 'delObject', callback, id)) { - try { - this.adapter.delForeignObject(id, {user: socket._acl.user}, (error, ...args) => - SocketCommands._fixCallback(callback, error, ...args)); - } catch (error) { - this.adapter.log.error(`[delObject] ERROR: ${error.toString()}`); - SocketCommands._fixCallback(callback, error); - } - } - } else { - SocketCommands._fixCallback(callback, SocketCommands.ERROR_PERMISSION); - } - }; - - this.commands['clientSubscribe'] = (socket, targetInstance, messageType, data, callback) => { - // Client informs specific instance about subscription on its messages. After subscription the socket will receive "im" messages from desired instance - // @param {string} targetInstance - instance name, e.g. "cameras.0" - // @param {string} messageType - message type, e.g. "startRecording/cam1" - // @param {object} data - optional data object, e.g. {width: 640, height: 480} - // @param {function} callback - `function (error, result)`, target instance MUST acknowledge the subscription and return some object as result - if (typeof data === 'function') { - callback = data; - data = null; - } - if (!targetInstance.startsWith('system.adapter.')) { - targetInstance = `system.adapter.${targetInstance}`; - } - const sid = socket.id; - // GUI subscribes for messages from targetInstance - this.clientSubscribes[sid] = this.clientSubscribes[sid] || {}; - this.clientSubscribes[sid][targetInstance] = this.clientSubscribes[sid][targetInstance] || []; - if (!this.clientSubscribes[sid][targetInstance].includes(messageType)) { - this.clientSubscribes[sid][targetInstance].push(messageType); - } - // inform instance about new subscription - this.adapter.sendTo(targetInstance, 'clientSubscribe', {type: messageType, sid, data}, result => - SocketCommands._fixCallback(callback, null, result)); - }; - - this.commands['clientUnsubscribe'] = (socket, targetInstance, messageType, callback) => { - // Client unsubscribes from specific instance's messages - // @param {string} targetInstance - instance name, e.g. "cameras.0" - // @param {string} messageType - message type, e.g. "startRecording/cam1" - // @param {function} callback - `function (error, wasSubscribed)`, target instance MUST NOT acknowledge the un-subscription - const sid = socket.id; - if (!targetInstance.startsWith('system.adapter.')) { - targetInstance = `system.adapter.${targetInstance}`; - } - - // GUI unsubscribes for messages from targetInstance - if (this.clientSubscribes[sid] && this.clientSubscribes[sid][targetInstance]) { - const pos = this.clientSubscribes[sid][targetInstance].indexOf(messageType); - if (pos !== -1) { - this.clientSubscribes[sid][targetInstance].splice(pos, 1); - // inform instance about unsubscription - this.adapter.sendTo(targetInstance, 'clientUnsubscribe', {type: [messageType], sid, reason: 'client'}); - SocketCommands._fixCallback(callback, null, true); - return; - } - } - SocketCommands._fixCallback(callback, null, false); - }; - } - - _initCommands() { - this.__initCommandsCommon(); - this.__initCommandsObjects(); - this.__initCommandsStates(); - this.__initCommandsFiles(); - } - - _informAboutDisconnect(socketId) { - // say to all instances, that this socket was disconnected - if (this.clientSubscribes[socketId]) { - Object.keys(this.clientSubscribes[socketId]).forEach(targetInstance => { - this.adapter.sendTo(targetInstance, 'clientUnsubscribe', { - message: this.clientSubscribes[socketId][targetInstance], - sid: socketId, - reason: 'disconnect', - }); - }); - delete this.clientSubscribes[socketId]; - } - } - - applyCommands(socket) { - Object.keys(this.commands) - .forEach(command => socket.on(command, (...args) => { - if (this._updateSession(socket)) { - this.commands[command](socket, ...args); - } - })); - } - - destroy() { - // could be overloaded - } -} - -module.exports = SocketCommands; diff --git a/lib/socketCommon.js b/lib/socketCommon.js deleted file mode 100644 index 0c79f31..0000000 --- a/lib/socketCommon.js +++ /dev/null @@ -1,435 +0,0 @@ -/** - * Class Socket - * - * Copyright 2014-2023 bluefox , - * MIT License - * - */ -const SocketCommands = require('./socketCommands'); - -class SocketCommon { - static COMMAND_RE_AUTHENTICATE = 'reauthenticate'; - - constructor(settings, adapter) { - this.settings = settings || {}; - this.adapter = adapter; - this.commands = null; - this.noDisconnect = this.__getIsNoDisconnect(); - this.infoTimeout = null; - this.eventHandlers = {}; - this.store = null; // will be set in __initAuthentication - this.adapter._language = this.settings.language; - } - - __getIsNoDisconnect() { - throw new Error('"__getIsNoDisconnect" must be implemented in SocketCommon!'); - } - - __initAuthentication(authOptions) { - throw new Error('"__initAuthentication" must be implemented in SocketCommon!'); - } - - // Extract username from socket - __getUserFromSocket(socket, callback) { - throw new Error('"__getUserFromSocket" must be implemented in SocketCommon!'); - } - - __getClientAddress(socket) { - throw new Error('"__getClientAddress" must be implemented in SocketCommon!'); - } - - // update session ID, but not ofter than 60 seconds - __updateSession(socket) { - throw new Error('"__updateSession" must be implemented in SocketCommon!'); - } - - __getSessionID(socket) { - throw new Error('"__getSessionID" must be implemented in SocketCommon!'); - } - - addEventHandler(eventName, handler) { - this.eventHandlers[eventName] = handler; - } - - start(server, socketClass, authOptions, socketOptions) { - this.serverMode = !!socketClass; - - this.commands = this.commands || new SocketCommands(this.adapter, socket => this.__updateSession(socket)); - - this.server = server; - - this.settings.defaultUser = this.settings.defaultUser || 'system.user.admin'; - if (!this.settings.defaultUser.match(/^system\.user\./)) { - this.settings.defaultUser = 'system.user.' + this.settings.defaultUser; - } - - this.settings.ttl = parseInt(this.settings.ttl, 10) || 3600; - - // it can be used as a client too for cloud - if (socketClass) { - if (!server.__inited) { - if (typeof socketClass.listen === 'function') { - // old socket.io@2.x and ws - this.server = socketClass.listen(server, socketOptions); - } else { - // socket.io 4.x - this.server = socketClass(server, socketOptions); - } - - if (typeof this.server.of === 'function') { - this.allNamespaces = this.server.of(/.*/); - } - - server.__inited = true; - this.adapter.log.info(`${this.settings.secure ? 'Secure ' : ''}socket.io server listening on port ${this.settings.port}`); - } - - if (this.settings.auth && this.server) { - this.__initAuthentication(authOptions); - } - - // Enable cross-domain access - // deprecated, because no more used in socket.io@4 only(in @2) - if (this.settings.crossDomain && this.server.set) { - this.server.set('origins', '*:*'); - } - - this.server.on('connection', (socket, cb) => { - this.eventHandlers.connect && this.eventHandlers.connect(socket); - this._initSocket(socket, cb); - }); - // support of dynamic namespaces (because of reverse proxy) - this.allNamespaces && this.allNamespaces.on('connection', (socket, cb) => { - this.eventHandlers.connect && this.eventHandlers.connect(socket); - this._initSocket(socket, cb); - }); - } - - this.server.on('error', (error, details) => { - // ignore "failed connection" as it already shown - if (!error || !error.message || !error.message.includes('failed connection')) { - if ((error && error.message && error.message.includes('authentication failed') || - (details && details.toString().includes('authentication failed'))) - ) { - this.adapter.log.debug(`Error: ${(error && error.message) || JSON.stringify(error)}${details ? ` - ${details}` : ''}`); - } else { - this.adapter.log.error(`Error: ${(error && error.message) || JSON.stringify(error)}${details ? ` - ${details}` : ''}`); - } - } - }); - - // support of dynamic namespaces (because of reverse proxy) - this.allNamespaces && this.allNamespaces.on('error', (error, details) => { - // ignore "failed connection" as it already shown - if (!error || !error.message || !error.message.includes('failed connection')) { - if (error && error.message && error.message.includes('authentication failed')) { - this.adapter.log.debug(`Error: ${(error && error.message) || JSON.stringify(error)}${details ? ` - ${details}` : ''}`); - } else { - this.adapter.log.error(`Error: ${(error && error.message) || JSON.stringify(error)}${details ? ` - ${details}` : ''}`); - } - } - }); - - this._updateConnectedInfo(); - } - - _initSocket(socket, cb) { - this.commands.disableEventThreshold && this.commands.disableEventThreshold(); - const address = this.__getClientAddress(socket); - - if (!socket._acl) { - if (this.settings.auth) { - this.__getUserFromSocket(socket, (err, user) => { - if (err || !user) { - socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE); - this.adapter.log.error(`socket.io [init] ${err || 'No user found in cookies'}`); - // ws does not require disconnect - if (!this.noDisconnect) { - socket.disconnect(); - } - } else { - socket._secure = true; - this.adapter.log.debug(`socket.io client ${user} connected`); - if (!user.startsWith('system.user.')) { - user = `system.user.${user}`; - } - this.adapter.calculatePermissions(user, SocketCommands.COMMANDS_PERMISSIONS, acl => { - socket._acl = SocketCommon._mergeACLs(address, acl, this.settings.whiteListSettings); - this._socketEvents(socket, address, cb); - }); - } - }); - } else { - this.adapter.calculatePermissions(this.settings.defaultUser, SocketCommands.COMMANDS_PERMISSIONS, acl => { - socket._acl = SocketCommon._mergeACLs(address, acl, this.settings.whiteListSettings); - this._socketEvents(socket, address, cb); - }); - } - } else { - this._socketEvents(socket, address, cb); - } - } - - unsubscribeSocket(socket, type) { - return this.commands.unsubscribeSocket(socket, type); - } - - _unsubscribeAll() { - if (this.server && this.server.ioBroker) { - // this could be an object or array - const sockets = this.server.sockets.sockets || this.server.sockets.connected; - - Object.keys(sockets).forEach(i => - this.commands.unsubscribeSocket(sockets[i])); - } else - if (this.server && this.server.sockets) { - for (const socket in this.server.sockets) { - if (Object.prototype.hasOwnProperty.call(this.server.sockets, socket)) { - this.commands.unsubscribeSocket(socket); - } - } - } - }; - - static getWhiteListIpForAddress(address, whiteList) { - if (!whiteList) { - return null; - } - - // check IPv6 or IPv4 direct match - if (Object.prototype.hasOwnProperty.call(whiteList, address)) { - return address; - } - - // check if the address is IPv4 - const addressParts = address.split('.'); - if (addressParts.length !== 4) { - return null; - } - - // do we have settings for wild-carded ips? - const wildCardIps = Object.keys(whiteList).filter(key => key.includes('*')); - - if (!wildCardIps.length) { - // no wild-carded ips => no ip configured - return null; - } - - wildCardIps.forEach(ip => { - const ipParts = ip.split('.'); - if (ipParts.length === 4) { - for (let i = 0; i < 4; i++) { - if (ipParts[i] === '*' && i === 3) { - // match - return ip; - } - - if (ipParts[i] !== addressParts[i]) { - break; - } - } - } - }); - - return null; - } - - static _getPermissionsForIp(address, whiteList) { - return whiteList[SocketCommon.getWhiteListIpForAddress(address, whiteList) || 'default']; - } - - static _mergeACLs(address, acl, whiteList) { - if (whiteList && address) { - const whiteListAcl = SocketCommon._getPermissionsForIp(address, whiteList); - if (whiteListAcl) { - ['object', 'state', 'file'].forEach(key => { - if (Object.prototype.hasOwnProperty.call(acl, key) && Object.prototype.hasOwnProperty.call(whiteListAcl, key)) { - Object.keys(acl[key]).forEach(permission => { - if (Object.prototype.hasOwnProperty.call(whiteListAcl[key], permission)) { - acl[key][permission] = acl[key][permission] && whiteListAcl[key][permission]; - } - }); - } - }); - - if (whiteListAcl.user !== 'auth') { - acl.user = `system.user.${whiteListAcl.user}`; - } - } - } - - return acl; - } - - // install event handlers on socket - _socketEvents(socket, address, cb) { - if (this.serverMode) { - this.adapter.log.info(`==> Connected ${socket._acl.user} from ${address}`); - } else { - this.adapter.log.info(`Trying to connect as ${socket._acl.user} to ${address}`); - } - - this._updateConnectedInfo(); - - if (!this.commands.getCommandHandler('name')) { - // socket sends its name => update list of sockets - this.addCommandHandler('name', (_socket, name, cb) => { - this.adapter.log.debug(`Connection from "${name}"`); - if (_socket._name === undefined) { - _socket._name = name; - this._updateConnectedInfo(); - } else if (_socket._name !== name) { - this.adapter.log.warn(`socket ${_socket.id} changed socket name from ${_socket._name} to ${name}`); - _socket._name = name; - this._updateConnectedInfo(); - } - - typeof cb === 'function' && cb(); - }); - } - - this.commands.applyCommands(socket); - - // disconnect - socket.on('disconnect', error => { - this.commands.unsubscribeSocket(socket); - this._updateConnectedInfo(); - - // Disable logging if no one browser is connected - if (this.adapter.requireLog && this.commands && this.commands.isLogEnabled()) { - this.adapter.log.debug('Disable logging, because no one socket connected'); - this.adapter.requireLog(!!(this.server && this.server.engine && this.server.engine.clientsCount)); - } - - if (socket._sessionTimer) { - clearTimeout(socket._sessionTimer); - socket._sessionTimer = null; - } - - if (this.eventHandlers.disconnect) { - this.eventHandlers.disconnect(socket, error); - } else { - this.adapter.log.info(`<== Disconnect ${socket._acl.user} from ${this.__getClientAddress(socket)} ${socket._name || ''}`); - } - }); - - if (typeof this.settings.extensions === 'function') { - this.settings.extensions(socket); - } - - // if server mode - if (this.serverMode) { - const sessionId = this.__getSessionID(socket); - if (sessionId) { - socket._secure = true; - socket._sessionID = sessionId; - // Get user for session - this.store && this.store.get(socket._sessionID, (err, obj) => { - if (!obj || !obj.passport) { - socket._acl.user = ''; - socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE); - // ws does not require disconnect - if (!this.noDisconnect) { - socket.disconnect(); - } - } - if (socket._authPending) { - socket._authPending(!!socket._acl.user, true); - delete socket._authPending; - } - }); - } - } - - this.commands.subscribeSocket(socket); - - cb && cb(); - } - - _updateConnectedInfo() { - // only in server mode - if (this.serverMode) { - if (this.infoTimeout) { - clearTimeout(this.infoTimeout); - this.infoTimeout = null; - } - this.infoTimeout = setTimeout(() => { - this.infoTimeout = null; - - if (this.server) { - let clientsArray = []; - if (this.server.sockets) { - // this could be an object or array - const sockets = this.server.sockets.sockets || this.server.sockets.connected; - - Object.keys(sockets).forEach(i => - clientsArray.push(sockets[i]._name || 'noname')); - } - const text = `[${clientsArray.length}]${clientsArray.join(', ')}`; - this.adapter.setState('info.connected', text, true); - } - }, 1000); - } - } - - checkPermissions(socket, command, callback, arg) { - return this.commands._checkPermissions(socket, command, callback, arg); - } - - addCommandHandler(command, handler) { - this.commands.addCommandHandler(command, handler); - } - - sendLog(obj) { - // TODO Build in some threshold - if (this.server && this.server.sockets) { - this.server.sockets.emit('log', obj); - } - } - - publish(socket, type, id, obj) { - return this.commands.publish(socket, type, id, obj); - } - - publishInstanceMessage(socket, sourceInstance, messageType, data) { - return this.commands.publishInstanceMessage(socket, sourceInstance, messageType, data); - } - - publishFile(socket, id, fileName, size) { - return this.commands.publishFile(socket, id, fileName, size); - } - - close() { - this._unsubscribeAll(); - - this.commands.destroy(); - - if (this.server && this.server.sockets) { - // this could be an object or array - const sockets = this.server.sockets.sockets || this.server.sockets.connected; - - Object.keys(sockets).forEach(i => { - const socket = sockets[i]; - if (socket._sessionTimer) { - clearTimeout(socket._sessionTimer); - socket._sessionTimer = null; - } - }); - } - - // IO server will be closed - try { - this.server && this.server.close && this.server.close(); - this.server = null; - } catch (e) { - // ignore - } - - if (this.infoTimeout) { - clearTimeout(this.infoTimeout); - this.infoTimeout = null; - } - } -} - -module.exports = SocketCommon; diff --git a/lib/socketWS.js b/lib/socketWS.js deleted file mode 100644 index 9845d92..0000000 --- a/lib/socketWS.js +++ /dev/null @@ -1,218 +0,0 @@ -const SocketCommon = require('@iobroker/socket-classes').SocketCommon; -let passport; // require('passport') - only if auth is activated -let cookieParser; // require('cookie-parser') - only if auth is activated -let passportSocketIo; // require('./passportSocket') - only if auth is activated - -// From settings used only secure, auth and crossDomain -class SocketWS extends SocketCommon { - __getIsNoDisconnect() { - return true; - } - - _onAuthorizeSuccess = (data, accept) => { - this.adapter.log.debug(`successful connection to socket.io from ${data.connection.remoteAddress}`); - accept(); - } - - _onAuthorizeFail = (data, message, error, accept) => { - setTimeout(() => data.socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE), 100); - - error && this.adapter.log.error(`failed connection to socket.io from ${data.connection.remoteAddress}:`, message); - - if (error) { - accept(new Error(message)); - } else { - accept(new Error(`failed connection to socket.io: ${message}`));//null, false); - } - // this error will be sent to the user as a special error-package - // see: http://socket.io/docs/client-api/#socket > error-object - } - - __initAuthentication(authOptions) { - passportSocketIo = passportSocketIo || require('@iobroker/socket-classes').passportSocket; - passport = passport || require('@iobroker/socket-classes').passport; - cookieParser = cookieParser || require('@iobroker/socket-classes').cookieParser; - - if (authOptions.store && !this.store) { - this.store = authOptions.store; - } else if (!authOptions.store && this.store) { - authOptions.store = this.store; - } - - this.server.use(passportSocketIo.authorize({ - passport, - cookieParser, - checkUser: authOptions.checkUser, - key: authOptions.userKey, // the name of the cookie where express/connect stores its session_id - secret: authOptions.secret, // the session_secret to parse the cookie - store: authOptions.store, // we NEED to use a sessionstore. no memorystore please - success: this._onAuthorizeSuccess, // *optional* callback on success - read more below - fail: this._onAuthorizeFail // *optional* callback on fail/error - read more below - })); - } - - // Extract username from socket - __getUserFromSocket(socket, callback) { - let wait = false; - if (typeof callback !== 'function') { - return; - } - - const user = socket.query.user; - const pass = socket.query.pass; - if (user && pass) { - wait = true; - this.adapter.checkPassword(user, pass, res => { - if (res) { - this.adapter.log.debug(`Logged in: ${user}`); - if (typeof callback === 'function') { - callback(null, user); - } else { - this.adapter.log.warn('[_getUserFromSocket] Invalid callback'); - } - } else { - this.adapter.log.warn(`Invalid password or user name: ${user}, ${pass[0]}***(${pass.length})`); - if (typeof callback === 'function') { - callback('unknown user'); - } else { - this.adapter.log.warn('[_getUserFromSocket] Invalid callback'); - } - } - }); - } else { - try { - if (socket.conn.request.sessionID) { - socket._sessionID = socket.conn.request.sessionID; - if (this.store) { - wait = true; - this.store.get(socket.conn.request.sessionID, (err, obj) => { - if (obj && obj.passport && obj.passport.user) { - callback(null, obj.passport.user ? `system.user.${obj.passport.user}` : ''); - } - }); - } - } - } catch { - // ignore - } - } - - !wait && callback('Cannot detect user'); - } - - __getClientAddress(socket) { - let address; - if (socket.connection) { - address = socket.connection && socket.connection.remoteAddress; - } else { - address = socket.ws._socket.remoteAddress; - } - - if (!address && socket.handshake) { - address = socket.handshake.address; - } - if (!address && socket.conn.request && socket.conn.request.connection) { - address = socket.conn.request.connection.remoteAddress; - } - return address; - } - - _waitForSessionEnd(socket) { - if (socket._sessionTimer) { - clearTimeout(socket._sessionTimer); - socket._sessionTimer = null; - } - const sessionId = socket._sessionID; - this.store && this.store.get(sessionId, (err, obj) => { - if (obj) { - const expires = new Date(obj.cookie.expires); - const interval = expires.getTime() - Date.now(); - if (interval > 0) { - socket._sessionTimer = socket._sessionTimer || setTimeout(() => this._waitForSessionEnd(socket), interval > 3600000 ? 3600000 : interval); - socket.emit('expire', expires.getTime()); - } else { - this.adapter.log.warn('REAUTHENTICATE!'); - socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE); - } - } else { - this.adapter.log.warn('REAUTHENTICATE!'); - socket && socket.emit && socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE); - } - }); - } - - // update session ID, but not ofter than 60 seconds - __updateSession(socket) { - const sessionId = socket._sessionID; - const now = Date.now(); - if (sessionId && (!socket._lastUpdate || now - socket._lastUpdate > 10000)) { - socket._lastUpdate = now; - this.store && this.store.get(sessionId, (err, obj) => { - // obj = {"cookie":{"originalMaxAge":2592000000,"expires":"2020-09-24T18:09:50.377Z","httpOnly":true,"path":"/"},"passport":{"user":"admin"}} - if (obj) { - // start timer - !socket._sessionTimer && this._waitForSessionEnd(socket); - /*obj.ttl = obj.ttl || (new Date(obj.cookie.expires).getTime() - now); - const expires = new Date(); - expires.setMilliseconds(expires.getMilliseconds() + obj.ttl + 10000); - obj.cookie.expires = expires.toISOString(); - console.log('Session ' + sessionId + ' expires on ' + obj.cookie.expires); - - this.store.set(sessionId, obj);*/ - } else { - this.adapter.log.warn('REAUTHENTICATE!'); - socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE); - } - }); - } - return true; - } - - __getSessionID(socket) { - return this.adapter.config.auth && socket._sessionID; - } - - publishAll(type, id, obj) { - if (id === undefined) { - console.log('Problem'); - } - - if (this.server && this.server.sockets) { - this.server.sockets.connected.forEach(socket => this.publish(socket, type, id, obj)); - } - } - - publishFileAll(id, fileName, size) { - if (id === undefined) { - console.log('Problem'); - } - - if (this.server && this.server.sockets) { - const sockets = this.server.sockets.sockets || this.server.sockets.connected; - - // this could be an object or array - Object.keys(sockets).forEach(i => { - if (this.publishFile(sockets[i], id, fileName, size)) { - this.__updateSession(sockets[i]); - } - }); - } - } - - publishInstanceMessageAll(sourceInstance, messageType, sid, data) { - if (this.server && this.server.sockets) { - const sockets = this.server.sockets.sockets || this.server.sockets.connected; - - // this could be an object or array - Object.keys(sockets).forEach(i => { - if (sockets[i].id === sid) { - if (this.publishInstanceMessage(sockets[i], sourceInstance, messageType, data)) { - this.__updateSession(sockets[i]); - } - } - }); - } - } -} - -module.exports = SocketWS; diff --git a/main.js b/main.js deleted file mode 100644 index 7dcef38..0000000 --- a/main.js +++ /dev/null @@ -1,290 +0,0 @@ -/* jshint -W097 */ -/* jshint strict: false */ -/* jslint node: true */ -'use strict'; - -const adapterName = require('./package.json').name.split('.').pop(); -const utils = require('@iobroker/adapter-core'); // Get common adapter utils -const SocketWS = require('./lib/socketWS.js'); -const { WebServer } = require('@iobroker/webserver'); -const ws = require('@iobroker/ws-server'); - -let webServer = null; -let store = null; -let secret = 'Zgfr56gFe87jJOM'; // Will be generated by first start -const bruteForce = {}; - -let adapter; -function startAdapter(options) { - options = options || {}; - - Object.assign(options, { name: adapterName }); - - adapter = new utils.Adapter(options); - - adapter.on('objectChange', (id, obj) => { - if (webServer && webServer.io) { - webServer.io.publishAll('objectChange', id, obj); - } - }); - - adapter.on('stateChange', (id, state) => { - if (webServer && webServer.io) { - webServer.io.publishAll('stateChange', id, state); - } - }); - - adapter.on('fileChange', (id, fileName, size) => { - if (webServer && webServer.io) { - webServer.io.publishFileAll(id, fileName, size); - } - }); - - adapter.on('unload', callback => { - try { - adapter.setState && adapter.setState('info.connected', '', true); - adapter.setState && adapter.setState('info.connection', false, true); - adapter.log.info( - `terminating http${webServer.settings.secure ? 's' : ''} server on port ${webServer.settings.port}` - ); - webServer.io.close(); - webServer.server.close(); - - callback(); - } catch { - callback(); - } - }); - - adapter.on('ready', () => { - if (adapter.config.auth) { - // Generate secret for session manager - adapter.getForeignObject('system.config', (err, obj) => { - if (!err && obj) { - if (!obj.native || !obj.native.secret) { - obj.native = obj.native || {}; - require('node:crypto').randomBytes(24, (ex, buf) => { - secret = buf.toString('hex'); - adapter.extendForeignObject('system.config', { native: { secret: secret } }); - main(); - }); - } else { - secret = obj.native.secret; - main(); - } - } else { - adapter.logger.error('Cannot find object system.config'); - } - }); - } else { - main(); - } - }); - - adapter.on('message', obj => { - if (!obj || obj.command !== 'im') { - // if not instance message - return; - } - - if (webServer && webServer.io) { - // to make messages shorter, we code the answer as: - // m - message type - // s - socket ID - // d - data - - webServer.io.publishInstanceMessageAll(obj.from, obj.message.m, obj.message.s, obj.message.d); - } - }); - - adapter.on('log', obj => webServer && webServer.io && webServer.io.sendLog(obj)); - - return adapter; -} - -function main() { - if (adapter.config.secure) { - // Load certificates - adapter.getCertificates((err, certificates, leConfig) => { - adapter.config.certificates = certificates; - adapter.config.leConfig = leConfig; - webServer = initWebServer(adapter.config); - }); - } else { - webServer = initWebServer(adapter.config); - } -} - -function checkUser(username, password, cb) { - username = (username || '') - .toString() - .replace(adapter.FORBIDDEN_CHARS, '_') - .replace(/\s/g, '_') - .replace(/\./g, '_') - .toLowerCase(); - - if (bruteForce[username] && bruteForce[username].errors > 4) { - let minutes = Date.now() - bruteForce[username].time; - if (bruteForce[username].errors < 7) { - if (Date.now() - bruteForce[username].time < 60000) { - minutes = 1; - } else { - minutes = 0; - } - } else if (bruteForce[username].errors < 10) { - if (Date.now() - bruteForce[username].time < 180000) { - minutes = Math.ceil((180000 - minutes) / 60000); - } else { - minutes = 0; - } - } else if (bruteForce[username].errors < 15) { - if (Date.now() - bruteForce[username].time < 600000) { - minutes = Math.ceil((600000 - minutes) / 60000); - } else { - minutes = 0; - } - } else if (Date.now() - bruteForce[username].time < 3600000) { - minutes = Math.ceil((3600000 - minutes) / 60000); - } else { - minutes = 0; - } - - if (minutes) { - return cb(`Too many errors. Try again in ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}.`, false); - } - } - - adapter.checkPassword(username, password, res => { - if (!res) { - bruteForce[username] = bruteForce[username] || { errors: 0 }; - bruteForce[username].time = Date.now(); - bruteForce[username].errors++; - } else if (bruteForce[username]) { - delete bruteForce[username]; - } - - if (res) { - return cb(null, username); - } else { - return cb(null, false); - } - }); -} - -//settings: { -// "port": 8080, -// "auth": false, -// "secure": false, -// "bind": "0.0.0.0", // "::" -//} -function initWebServer(settings) { - const server = { - app: null, - server: null, - io: null, - settings - }; - - settings.port = parseInt(settings.port, 10) || 0; - - if (settings.port) { - if (settings.secure && !settings.certificates) { - return null; - } - - settings.crossDomain = true; - settings.ttl = settings.ttl || 3600; - settings.forceWebSockets = settings.forceWebSockets || false; - - if (settings.auth) { - const session = require('express-session'); - const AdapterStore = utils.commonTools.session(session, settings.ttl); - // Authentication checked by server itself - store = new AdapterStore({ adapter: adapter }); - } - - adapter.getPort( - settings.port, - !settings.bind || settings.bind === '0.0.0.0' ? undefined : settings.bind || undefined, - async port => { - if (parseInt(port, 10) !== settings.port && !adapter.config.findNextPort) { - adapter.log.error(`port ${settings.port} already in use`); - return adapter.terminate - ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) - : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - } - - settings.port = port; - - try { - const webserver = new WebServer({ - app: server.app, - adapter, - secure: adapter.config.secure - }); - - server.server = await webserver.init(); - } catch (err) { - adapter.log.error(`Cannot create webserver: ${err}`); - adapter.terminate - ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) - : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - return; - } - if (!server.server) { - adapter.log.error(`Cannot create webserver`); - adapter.terminate - ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) - : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - return; - } - - let serverListening = false; - server.server.on('error', e => { - if (e.toString().includes('EACCES') && port <= 1024) { - adapter.log.error( - `node.js process has no rights to start server on the port ${port}.\n` + - 'Do you know that on linux you need special permissions for ports under 1024?\n' + - 'You can call in shell following scrip to allow it for node.js: "iobroker fix"' - ); - } else { - adapter.log.error(`Cannot start server on ${settings.bind || '0.0.0.0'}:${port}: ${e}`); - } - if (!serverListening) { - adapter.terminate - ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) - : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - } - }); - - // Start the web server - server.server.listen( - settings.port, - !settings.bind || settings.bind === '0.0.0.0' ? undefined : settings.bind || undefined, - () => { - adapter.setState('info.connection', true, true); - serverListening = true; - } - ); - - server.io = new SocketWS(settings, adapter); - server.io.start(server.server, ws, { userKey: 'connect.sid', checkUser, store, secret }); - } - ); - } else { - adapter.log.error('port missing'); - adapter.terminate - ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) - : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); - } - - return server; -} - -// If started as allInOne mode => return function to create instance -if (module.parent) { - module.exports = startAdapter; -} else { - // or start the instance directly - startAdapter(); -} diff --git a/package.json b/package.json index e9c1b30..3b1621c 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,69 @@ { - "name": "iobroker.ws", - "version": "2.7.0", - "description": "This adapter allows to communicate different web applications with ioBroker.", - "author": { - "name": "bluefox", - "email": "dogafox@gmail.com" - }, - "homepage": "https://github.com/ioBroker/ioBroker.ws", - "keywords": [ - "ioBroker", - "web" - ], - "repository": { - "type": "git", - "url": "https://github.com/ioBroker/ioBroker.ws" - }, - "engines": { - "node": ">=18" - }, - "dependencies": { - "@iobroker/adapter-core": "^3.2.2", - "@iobroker/socket-classes": "1.6.1", - "@iobroker/webserver": "^1.0.6", - "@iobroker/ws-server": "^4.1.0", - "express-session": "^1.18.1" - }, - "devDependencies": { - "@alcalzone/release-script": "^3.8.0", - "@alcalzone/release-script-plugin-iobroker": "^3.7.2", - "@alcalzone/release-script-plugin-license": "^3.7.0", - "@foxriver76/eslint-config": "^1.0.5", - "@iobroker/adapter-dev": "^1.3.0", - "@iobroker/legacy-testing": "^2.0.1", - "@iobroker/testing": "^5.0.0", - "@iobroker/ws": "^2.0.0", - "chai": "^4.5.0", - "mocha": "^10.8.2" - }, - "bugs": { - "url": "https://github.com/ioBroker/ioBroker.ws/issues" - }, - "main": "main.js", - "files": [ - "admin/", - "lib/", - "io-package.json", - "LICENSE", - "main.js" - ], - "scripts": { - "test": "mocha --exit", - "build": "node tasks", - "release": "release-script", - "release-patch": "release-script patch --yes", - "release-minor": "release-script minor --yes", - "release-major": "release-script major --yes", - "update-packages": "ncu --upgrade" - }, - "license": "MIT", - "readmeFilename": "README.md" + "name": "iobroker.ws", + "version": "2.7.0", + "description": "This adapter allows to communicate different web applications with ioBroker.", + "author": { + "name": "bluefox", + "email": "dogafox@gmail.com" + }, + "homepage": "https://github.com/ioBroker/ioBroker.ws", + "keywords": [ + "ioBroker", + "web" + ], + "repository": { + "type": "git", + "url": "https://github.com/ioBroker/ioBroker.ws" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@iobroker/adapter-core": "^3.2.3", + "@iobroker/socket-classes": "^2.0.4", + "@iobroker/webserver": "^1.0.8", + "@iobroker/ws-server": "^4.2.4", + "express-session": "^1.18.1" + }, + "devDependencies": { + "@alcalzone/release-script": "^3.8.0", + "@alcalzone/release-script-plugin-iobroker": "^3.7.2", + "@alcalzone/release-script-plugin-license": "^3.7.0", + "@iobroker/adapter-dev": "^1.3.0", + "@iobroker/eslint-config": "^1.0.0", + "@iobroker/legacy-testing": "^2.0.2", + "@iobroker/testing": "^5.0.3", + "@iobroker/types": "^7.0.6", + "@iobroker/ws": "^2.0.0", + "@iobroker/ws-server": "^4.2.4", + "@types/cookie-parser": "^1.4.8", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.1", + "@types/node": "^22.13.1", + "@types/passport": "^1.0.17", + "chai": "^4.5.0", + "mocha": "^11.1.0" + }, + "bugs": { + "url": "https://github.com/ioBroker/ioBroker.ws/issues" + }, + "main": "dist/main.js", + "files": [ + "admin/", + "dist/", + "io-package.json", + "LICENSE" + ], + "scripts": { + "test": "mocha --exit", + "build": "tsc -p tsconfig.build.json && node tasks", + "lint": "eslint -c eslint.config.mjs", + "release": "release-script", + "release-patch": "release-script patch --yes", + "release-minor": "release-script minor --yes", + "release-major": "release-script major --yes", + "update-packages": "npx -y npm-check-updates --upgrade" + }, + "license": "MIT", + "readmeFilename": "README.md" } diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index adbbdcd..0000000 --- a/prettier.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@foxriver76/eslint-config/prettier'); \ No newline at end of file diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 0000000..2f00708 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,3 @@ +import prettierConfig from '@iobroker/eslint-config/prettier.config.mjs'; + +export default prettierConfig; diff --git a/src/lib/socketWS.ts b/src/lib/socketWS.ts new file mode 100644 index 0000000..16692d8 --- /dev/null +++ b/src/lib/socketWS.ts @@ -0,0 +1,285 @@ +import { SocketCommon, passportSocket, type PassportHttpRequest, type Store } from '@iobroker/socket-classes'; +import type { Socket as WebSocketClient } from '@iobroker/ws-server'; +import passport from 'passport'; +import cookieParser from 'cookie-parser'; +import type { AddressInfo } from 'node:net'; +import type { WsAdapterConfig } from '../types'; +import type { SocketSubscribeTypes } from '@iobroker/socket-classes/dist/types'; + +// From settings used only secure, auth and crossDomain +export class SocketWS extends SocketCommon { + __getIsNoDisconnect(): boolean { + return true; + } + + #onAuthorizeSuccess = (data: PassportHttpRequest, accept: (err: boolean) => void): void => { + this.adapter.log.debug( + `successful connection to socket.io from ${(data.socket || data.connection).remoteAddress}`, + ); + accept(false); + }; + + #onAuthorizeFail = ( + data: PassportHttpRequest, + message: string, + critical: boolean, + accept: (err: boolean) => void, + ): void => { + setTimeout(() => data.socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE), 100); + + if (critical) { + this.adapter?.log.info( + `failed connection to socket.io from ${(data.socket || data.connection).remoteAddress}: ${message}`, + ); + } + + // this error will be sent to the user as a special error-package + // see: http://socket.io/docs/client-api/#socket > error-object + if (critical) { + // @ts-expect-error + accept(new Error(message)); + } else { + // @ts-expect-error + accept(new Error(`failed connection to socket.io: ${message}`)); //null, false); + } + }; + + __initAuthentication(authOptions: { + store: Store; + secret: string; + checkUser?: ( + user: string, + pass: string, + cb: ( + error: Error | null, + result?: { + logged_in: boolean; + }, + ) => void, + ) => void; + }): void { + if (authOptions.store && !this.store) { + this.store = authOptions.store; + } else if (!authOptions.store && this.store) { + authOptions.store = this.store; + } + + this.server?.use( + passportSocket({ + passport, + cookieParser, + checkUser: authOptions.checkUser, + secret: authOptions.secret, // the session_secret to parse the cookie + store: authOptions.store, // we NEED to use a sessionstore. no memorystore, please + success: this.#onAuthorizeSuccess, // *optional* callback on success - read more below + fail: this.#onAuthorizeFail, // *optional* callback on fail/error - read more below + }), + ); + } + + // Extract username from socket + __getUserFromSocket(socket: WebSocketClient, callback: (error: string | null, user?: string) => void): void { + let wait = false; + if (typeof callback !== 'function') { + return; + } + + const user = socket.query.user; + const pass = socket.query.pass; + if (user && typeof user === 'string' && pass && typeof pass === 'string') { + wait = true; + void this.adapter.checkPassword(user, pass, res => { + if (res) { + this.adapter.log.debug(`Logged in: ${user}`); + if (typeof callback === 'function') { + callback(null, user); + } else { + this.adapter.log.warn('[_getUserFromSocket] Invalid callback'); + } + } else { + this.adapter.log.warn(`Invalid password or user name: ${user}, ${pass[0]}***(${pass.length})`); + if (typeof callback === 'function') { + callback('unknown user'); + } else { + this.adapter.log.warn('[_getUserFromSocket] Invalid callback'); + } + } + }); + } else { + try { + if (socket.conn.request.sessionID) { + socket._sessionID = socket.conn.request.sessionID; + if (this.store) { + wait = true; + this.store.get(socket.conn.request.sessionID, (_err, obj) => { + if (obj?.passport?.user) { + callback(null, obj.passport.user ? `system.user.${obj.passport.user}` : ''); + } + }); + } + } + } catch { + // ignore + } + } + + !wait && callback('Cannot detect user'); + } + + __getClientAddress(socket: WebSocketClient): AddressInfo { + let address; + if (socket.connection) { + address = socket.connection && socket.connection.remoteAddress; + } else { + // @ts-expect-error socket.io + address = socket.ws._socket.remoteAddress; + } + + // @ts-expect-error socket.io + if (!address && socket.handshake) { + // @ts-expect-error socket.io + address = socket.handshake.address; + } + // @ts-expect-error socket.io + if (!address && socket.conn.request?.connection) { + // @ts-expect-error socket.io + address = socket.conn.request.connection.remoteAddress; + } + return address; + } + + #waitForSessionEnd(socket: WebSocketClient): void { + if (socket._sessionTimer) { + clearTimeout(socket._sessionTimer); + socket._sessionTimer = undefined; + } + const sessionId = socket._sessionID; + if (sessionId) { + this.store?.get( + sessionId, + ( + _err: Error | null, + obj: { + cookie: { + originalMaxAge: number; + expires: string; + httpOnly: boolean; + path: string; + }; + passport: { + user: string; + }; + }, + ) => { + if (obj) { + const expires = new Date(obj.cookie.expires); + const interval = expires.getTime() - Date.now(); + if (interval > 0) { + socket._sessionTimer ||= setTimeout( + () => this.#waitForSessionEnd(socket), + interval > 3600000 ? 3600000 : interval, + ); + socket.emit('expire', expires.getTime()); + } else { + this.adapter.log.warn('REAUTHENTICATE!'); + socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE); + } + } else { + this.adapter.log.warn('REAUTHENTICATE!'); + socket?.emit?.(SocketCommon.COMMAND_RE_AUTHENTICATE); + } + }, + ); + } else { + socket?.emit?.(SocketCommon.COMMAND_RE_AUTHENTICATE); + } + } + + // update session ID, but not ofter than 60 seconds + __updateSession(socket: WebSocketClient): boolean { + const sessionId = socket._sessionID; + const now = Date.now(); + if (sessionId && (!socket._lastActivity || now - socket._lastActivity > 10000)) { + socket._lastActivity = now; + this.store?.get( + sessionId, + ( + _err: Error | null, + obj: { + cookie: { + originalMaxAge: number; + expires: string; + httpOnly: boolean; + path: string; + }; + passport: { + user: string; + }; + }, + ): void => { + // obj = {"cookie":{"originalMaxAge":2592000000,"expires":"2020-09-24T18:09:50.377Z","httpOnly":true,"path":"/"},"passport":{"user":"admin"}} + if (obj) { + // start timer + if (!socket._sessionTimer) { + this.#waitForSessionEnd(socket); + } + /*obj.ttl = obj.ttl || (new Date(obj.cookie.expires).getTime() - now); + const expires = new Date(); + expires.setMilliseconds(expires.getMilliseconds() + obj.ttl + 10000); + obj.cookie.expires = expires.toISOString(); + console.log('Session ' + sessionId + ' expires on ' + obj.cookie.expires); + + this.store.set(sessionId, obj);*/ + } else { + this.adapter.log.warn('REAUTHENTICATE!'); + socket.emit(SocketCommon.COMMAND_RE_AUTHENTICATE); + } + }, + ); + } + return true; + } + + __getSessionID(socket: WebSocketClient): string | null { + return (this.adapter.config as WsAdapterConfig).auth ? socket._sessionID || null : null; + } + + publishAll(type: SocketSubscribeTypes, id: string, obj: ioBroker.Object | ioBroker.State | null | undefined): void { + if (id === undefined) { + console.log('Problem'); + } + + this.server?.sockets?.connected.forEach(socket => this.publish(socket, type, id, obj)); + } + + publishFileAll(id: string, fileName: string, size: number | null): void { + if (id === undefined) { + console.log('Problem'); + } + + if (this.server?.sockets) { + const sockets = this.server.sockets.sockets || this.server.sockets.connected; + + for (const socket of sockets) { + if (this.publishFile(socket, id, fileName, size)) { + this.__updateSession(socket); + } + } + } + } + + publishInstanceMessageAll(sourceInstance: string, messageType: string, sid: string, data: any): void { + if (this.server?.sockets) { + const sockets = this.server.sockets.sockets || this.server.sockets.connected; + + // this could be an object or array + for (const socket of sockets) { + if (socket.id === sid) { + if (this.publishInstanceMessage(socket, sourceInstance, messageType, data)) { + this.__updateSession(socket); + } + } + } + } + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..2d719f2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,326 @@ +import { randomBytes } from 'node:crypto'; +import type { IncomingMessage, OutgoingMessage, Server as HttpServer } from 'node:http'; +import type { Server as HttpsServer } from 'node:https'; +import * as session from 'express-session'; +import { Adapter, type AdapterOptions, commonTools, EXIT_CODES } from '@iobroker/adapter-core'; // Get common adapter utils +import { WebServer } from '@iobroker/webserver'; +import { SocketIO, type Socket as WebSocketClient } from '@iobroker/ws-server'; +import type { Store } from '@iobroker/socket-classes'; +import type { WsAdapterConfig } from './types'; +import { SocketWS } from './lib/socketWS'; +import { readFileSync } from 'node:fs'; + +type Server = HttpServer | HttpsServer; + +export class WsAdapter extends Adapter { + private wsConfig: WsAdapterConfig; + private server: { + server: null | Server; + io: null | SocketWS; + app: ((req: IncomingMessage, res: OutgoingMessage) => void) | null; + } = { + server: null, + io: null, + app: null, + }; + private readonly socketIoFile: string; + private bruteForce: { [ip: string]: { errors: number; time: number } } = {}; + private store: Store | null = null; + private secret = 'Zgfr56gFe87jJOM'; + private certificates: ioBroker.Certificates | undefined; + + public constructor(options: Partial = {}) { + super({ + ...options, + name: 'ws', + unload: callback => this.onUnload(callback), + message: obj => this.onMessage(obj), + stateChange: (id, state) => { + this.server?.io?.publishAll('stateChange', id, state); + }, + ready: () => this.main(), + objectChange: (id: string, obj: ioBroker.Object | null | undefined): void => { + this.server?.io?.publishAll('objectChange', id, obj); + }, + fileChange: (id: string, fileName: string, size: number | null): void => { + this.server?.io?.publishFileAll(id, fileName, size); + }, + }); + + this.socketIoFile = readFileSync(`${__dirname}/lib/socket.io.js`).toString('utf-8'); + this.wsConfig = this.config as WsAdapterConfig; + this.on('log', (obj: ioBroker.LogMessage): void => this.server?.io?.sendLog(obj)); + } + + onUnload(callback: () => void): void { + try { + void this.setState('info.connected', '', true); + void this.setState('info.connection', false, true); + this.log.info(`terminating http${this.wsConfig.secure ? 's' : ''} server on port ${this.wsConfig.port}`); + this.server.io?.close(); + this.server.server?.close(); + + callback(); + } catch { + callback(); + } + } + + onMessage(obj: ioBroker.Message): void { + if (obj?.command !== 'im') { + // if not instance message + return; + } + + // to make messages shorter, we code the answer as: + // m - message type + // s - socket ID + // d - data + this.server?.io?.publishInstanceMessageAll(obj.from, obj.message.m, obj.message.s, obj.message.d); + } + + checkUser( + username: string, + password: string, + cb: ( + error: null | Error, + result?: { + logged_in: boolean; + }, + ) => void, + ): void { + username = (username || '') + .toString() + .replace(this.FORBIDDEN_CHARS, '_') + .replace(/\s/g, '_') + .replace(/\./g, '_') + .toLowerCase(); + + if (this.bruteForce[username] && this.bruteForce[username].errors > 4) { + let minutes = Date.now() - this.bruteForce[username].time; + if (this.bruteForce[username].errors < 7) { + if (Date.now() - this.bruteForce[username].time < 60000) { + minutes = 1; + } else { + minutes = 0; + } + } else if (this.bruteForce[username].errors < 10) { + if (Date.now() - this.bruteForce[username].time < 180000) { + minutes = Math.ceil((180000 - minutes) / 60000); + } else { + minutes = 0; + } + } else if (this.bruteForce[username].errors < 15) { + if (Date.now() - this.bruteForce[username].time < 600000) { + minutes = Math.ceil((600000 - minutes) / 60000); + } else { + minutes = 0; + } + } else if (Date.now() - this.bruteForce[username].time < 3600000) { + minutes = Math.ceil((3600000 - minutes) / 60000); + } else { + minutes = 0; + } + + if (minutes) { + return cb( + new Error(`Too many errors. Try again in ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}.`), + ); + } + } + + void this.checkPassword(username, password, (success: boolean, _user: string): void => { + if (!success) { + this.bruteForce[username] = this.bruteForce[username] || { errors: 0 }; + this.bruteForce[username].time = Date.now(); + this.bruteForce[username].errors++; + } else if (this.bruteForce[username]) { + delete this.bruteForce[username]; + } + + if (success) { + return cb(null, { logged_in: true }); + } + return cb(null); + }); + } + + initWebServer(): void { + this.wsConfig.port = parseInt(this.wsConfig.port as string, 10) || 0; + + if (this.wsConfig.port) { + if (this.wsConfig.secure && !this.certificates) { + return; + } + + this.wsConfig.ttl = this.wsConfig.ttl || 3600; + + if (this.wsConfig.auth) { + const AdapterStore = commonTools.session(session, this.wsConfig.ttl); + // Authentication checked by server itself + this.store = new AdapterStore({ adapter: this }); + } + + this.getPort( + this.wsConfig.port, + !this.wsConfig.bind || this.wsConfig.bind === '0.0.0.0' ? undefined : this.wsConfig.bind || undefined, + async (port: number): Promise => { + if (parseInt(port as unknown as string, 10) !== this.wsConfig.port) { + this.log.error(`port ${this.wsConfig.port} already in use`); + return this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + + this.server.app = (req: IncomingMessage, res: OutgoingMessage): void => { + if (req.url?.includes('socket.io.js')) { + // @ts-expect-error + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(this.socketIoFile); + } else { + // @ts-expect-error + res.writeHead(404); + res.end('Not found'); + } + }; + + try { + const webserver = new WebServer({ + adapter: this, + secure: this.wsConfig.secure, + app: this.server.app, + }); + + this.server.server = await webserver.init(); + } catch (err) { + this.log.error(`Cannot create server: ${err}`); + this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + return; + } + if (!this.server.server) { + this.log.error(`Cannot create server`); + this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + return; + } + + let serverListening = false; + this.server.server.on('error', e => { + if (e.toString().includes('EACCES') && port <= 1024) { + this.log.error( + `node.js process has no rights to start server on the port ${port}.\n` + + 'Do you know that on linux you need special permissions for ports under 1024?\n' + + 'You can call in shell following scrip to allow it for node.js: "iobroker fix"', + ); + } else { + this.log.error(`Cannot start server on ${this.wsConfig.bind || '0.0.0.0'}:${port}: ${e}`); + } + if (!serverListening) { + this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + }); + + // Start the web server + this.server.server.listen( + this.wsConfig.port, + !this.wsConfig.bind || this.wsConfig.bind === '0.0.0.0' + ? undefined + : this.wsConfig.bind || undefined, + () => { + void this.setState('info.connection', true, true); + serverListening = true; + }, + ); + + const settings: { + language?: ioBroker.Languages; + defaultUser?: string; + ttl?: number; + secure?: boolean; + auth?: boolean; + crossDomain?: boolean; + extensions?: (socket: WebSocketClient) => void; + port?: number; + compatibilityV2?: boolean; + forceWebSockets?: boolean; + } = { + ttl: this.wsConfig.ttl as number, + port: this.wsConfig.port, + secure: this.wsConfig.secure, + auth: this.wsConfig.auth, + crossDomain: true, + forceWebSockets: true, // this is irrelevant for ws + defaultUser: this.wsConfig.defaultUser, + }; + + this.server.io = new SocketWS(settings, this); + this.server.io.start(this.server.server, SocketIO, { + checkUser: this.checkUser, + store: this.store!, + secret: this.secret, + }); + }, + ); + } else { + this.log.error('port missing'); + this.terminate + ? this.terminate(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) + : process.exit(EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); + } + } + + async main(): Promise { + this.wsConfig = this.config as WsAdapterConfig; + + if (this.wsConfig.auth) { + // Generate secret for session manager + const systemConfig = await this.getForeignObjectAsync('system.config'); + if (systemConfig) { + if (!systemConfig.native?.secret) { + systemConfig.native = systemConfig.native || {}; + await new Promise(resolve => + randomBytes(24, (_err: Error | null, buf: Buffer): void => { + this.secret = buf.toString('hex'); + void this.extendForeignObject('system.config', { native: { secret: this.secret } }); + resolve(); + }), + ); + } else { + this.secret = systemConfig.native.secret; + } + } else { + this.log.error('Cannot find object system.config'); + } + } + + if (this.wsConfig.secure) { + // Load certificates + await new Promise(resolve => + this.getCertificates( + undefined, + undefined, + undefined, + (_err: Error | null | undefined, certificates: ioBroker.Certificates | undefined): void => { + this.certificates = certificates; + resolve(); + }, + ), + ); + } + + this.initWebServer(); + } +} + +if (require.main !== module) { + // Export the constructor in compact mode + module.exports = (options: Partial | undefined) => new WsAdapter(options); +} else { + // otherwise start the instance directly + (() => new WsAdapter())(); +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..9fb8559 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,14 @@ +export interface WsAdapterConfig { + port: number | string; + auth: boolean; + secure: boolean; + bind: string; + ttl: number | string; + certPublic: string; + certPrivate: string; + certChained: string; + defaultUser: string; + leEnabled: boolean; + leUpdate: boolean; + leCheckPort: number | string; +} diff --git a/tasks.js b/tasks.js index 57b7d3b..4fb971b 100644 --- a/tasks.js +++ b/tasks.js @@ -1,4 +1,4 @@ -const { writeFileSync, readFileSync } = require('fs'); +const { writeFileSync, readFileSync } = require('node:fs'); const socket = require.resolve('@iobroker/ws').replace(/\\/g, '/'); -writeFileSync(`${__dirname}/lib/socket.io.js`, readFileSync(socket)); +writeFileSync(`${__dirname}/dist/lib/socket.io.js`, readFileSync(socket)); diff --git a/test/mocha.setup.js b/test/mocha.setup.js index 2adcb98..f641040 100644 --- a/test/mocha.setup.js +++ b/test/mocha.setup.js @@ -1 +1,3 @@ -process.on("unhandledRejection", (r) => { throw r; }); +process.on('unhandledRejection', r => { + throw r; +}); diff --git a/test/testAdapter.js b/test/testAdapter.js index 78433f1..c47880f 100644 --- a/test/testAdapter.js +++ b/test/testAdapter.js @@ -5,9 +5,8 @@ const expect = require('chai').expect; const setup = require('@iobroker/legacy-testing'); let objects = null; -let states = null; +let states = null; let onStateChanged = null; -let onObjectChanged = null; let sendToID = 1; const adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.') + 1); @@ -25,60 +24,19 @@ function checkConnectionOfAdapter(cb, counter) { if (state && state.val) { cb && cb(); } else { - setTimeout(() => - checkConnectionOfAdapter(cb, counter + 1), 1000); + setTimeout(() => checkConnectionOfAdapter(cb, counter + 1), 1000); } }); } -function checkValueOfState(id, value, cb, counter) { - counter = counter || 0; - if (counter > 20) { - return cb && cb(`Cannot check value Of State ${id}`); - } - - states.getState(id, (err, state) => { - if (err) console.error(err); - if (value === null && !state) { - cb && cb(); - } else - if (state && (value === undefined || state.val === value)) { - cb && cb(); - } else { - setTimeout(() => - checkValueOfState(id, value, cb, counter + 1), 500); - } - }); -} - -function sendTo(target, command, message, callback) { - onStateChanged = function (id, state) { - if (id === 'messagebox.system.adapter.test.0') { - callback(state.message); - } - }; - - states.pushMessage(`system.adapter.${target}`, { - command, - message, - from: 'system.adapter.test.0', - callback: { - message, - id: sendToID++, - ack: false, - time: Date.now() - } - }); -} - -describe(`Test ${adapterShortName} adapter`, function() { +describe(`Test ${adapterShortName} adapter`, function () { before(`Test ${adapterShortName} adapter: Start js-controller`, function (_done) { - this.timeout(600000); // because of first install from npm + this.timeout(600000); // because of the first installation from npm setup.setupController(async () => { const config = await setup.getAdapterConfig(); // enable adapter - config.common.enabled = true; + config.common.enabled = true; config.common.loglevel = 'debug'; //config.native.dbtype = 'sqlite'; @@ -91,9 +49,10 @@ describe(`Test ${adapterShortName} adapter`, function() { (id, state) => onStateChanged && onStateChanged(id, state), (_objects, _states) => { objects = _objects; - states = _states; + states = _states; _done(); - }); + }, + ); }); }); @@ -118,9 +77,9 @@ describe(`Test ${adapterShortName} adapter`, function() { done(); }); }); -/**/ + /**/ -/* + /* PUT YOUR OWN TESTS HERE USING it('Testname', function ( done) { ... @@ -137,4 +96,4 @@ describe(`Test ${adapterShortName} adapter`, function() { done(); }); }); -}); \ No newline at end of file +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..66c7731 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false, + "noEmit": false, + }, +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..55eaacb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +// Root tsconfig to set the settings and power editor support for all TS files +{ + "compileOnSave": true, + "compilerOptions": { + // do not compile anything; this file is just to configure type checking + // the compilation is configured in tsconfig.build.json + "noEmit": true, + // check JS files, but do not compile them => tsconfig.build.json + "allowJs": true, + "checkJs": true, + "skipLibCheck": true, // Don't report errors in 3rd party definitions + "noEmitOnError": true, + "declaration": true, + "outDir": "./dist/", + "removeComments": false, + "module": "Node16", + "moduleResolution": "node16", + "esModuleInterop": true, + // this is necessary for the automatic typing of the adapter config + "resolveJsonModule": true, + "strict": true, + "target": "es2022", + "sourceMap": true, + "inlineSourceMap": false, + "useUnknownInCatchVariables": false, + "types": ["@iobroker/types", "node"] + }, + "include": ["src/**/*.ts", "src/*.d.ts"], + "exclude": ["dist/**", "node_modules/**", "eslint.config.mjs", "example/**/*"] +}