├── .releaseconfig.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── auto-merge.yml ├── workflows │ ├── dependabot-automerge.yml │ └── test-and-release.yml └── stale.yml ├── .npmignore ├── example ├── refresh.js └── example.js ├── .eslintrc.json ├── LICENSE ├── package.json ├── .gitignore ├── README.md ├── lib └── proxy.js └── alexa-cookie.js /.releaseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["license"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Apollon77 4 | patreon: Apollon77 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Gruntfile.js 2 | tasks 3 | node_modules 4 | .git 5 | .idea 6 | package-lock.json 7 | .github 8 | lib/formerDataStore.json -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | time: "04:00" 8 | timezone: Europe/Berlin 9 | - package-ecosystem: npm 10 | directory: "/" 11 | schedule: 12 | interval: monthly 13 | time: "04:00" 14 | timezone: Europe/Berlin 15 | open-pull-requests-limit: 20 16 | versioning-strategy: increase -------------------------------------------------------------------------------- /example/refresh.js: -------------------------------------------------------------------------------- 1 | /* jshint -W097 */ 2 | /* jshint -W030 */ 3 | /* jshint strict: false */ 4 | /* jslint node: true */ 5 | /* jslint esversion: 6 */ 6 | 7 | const alexaCookie = require('../alexa-cookie'); 8 | 9 | const config = { 10 | logger: console.log, 11 | formerRegistrationData: { ... } // required: provide the result object from subsequent proxy usages here and some generated data will be reused for next proxy call too 12 | }; 13 | 14 | 15 | alexaCookie.refreshAlexaCookie(config, (err, result) => { 16 | console.log('RESULT: ' + err + ' / ' + JSON.stringify(result)); 17 | }); 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # Configure here which dependency updates should be merged automatically. 2 | # The recommended configuration is the following: 3 | - match: 4 | # Only merge patches for production dependencies 5 | dependency_type: production 6 | update_type: "semver:patch" 7 | - match: 8 | # Except for security fixes, here we allow minor patches 9 | dependency_type: production 10 | update_type: "security:minor" 11 | - match: 12 | # and development dependencies can have a minor update, too 13 | dependency_type: development 14 | update_type: "semver:minor" 15 | 16 | # The syntax is based on the legacy dependabot v1 automerged_updates syntax, see: 17 | # https://dependabot.com/docs/config-file/#automerged_updates -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | # Automatically merge Dependabot PRs when version comparison is within the range 2 | # that is configured in .github/auto-merge.yml 3 | 4 | name: Auto-Merge Dependabot PRs 5 | 6 | on: 7 | pull_request_target: 8 | 9 | jobs: 10 | auto-merge: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Check if PR should be auto-merged 17 | uses: ahmadnassri/action-dependabot-auto-merge@v2 18 | with: 19 | # This must be a personal access token with push access 20 | github-token: ${{ secrets.AUTO_MERGE_TOKEN }} 21 | # By default, squash and merge, so Github chooses nice commit messages 22 | command: squash and merge -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended" 10 | ], 11 | "plugins": [], 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 4, 16 | { 17 | "SwitchCase": 1 18 | } 19 | ], 20 | "no-console": "off", 21 | "no-var": "error", 22 | "no-trailing-spaces": "error", 23 | "prefer-const": "error", 24 | "quotes": [ 25 | "error", 26 | "single", 27 | { 28 | "avoidEscape": true, 29 | "allowTemplateLiterals": true 30 | } 31 | ], 32 | "semi": [ 33 | "error", 34 | "always" 35 | ] 36 | }, 37 | "parserOptions": { 38 | "ecmaVersion": 2018 39 | } 40 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something is not working as it should 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '...' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots & Logfiles** 23 | If applicable, add screenshots and logfiles to help explain your problem. 24 | 25 | **Versions:** 26 | - Adapter version: 27 | - JS-Controller version: 28 | - Node version: 29 | - Operating system: 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2025 Apollon77 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-cookie2", 3 | "version": "5.0.3", 4 | "description": "Generate Cookie and CSRF for Alexa Remote", 5 | "author": { 6 | "name": "Apollon77", 7 | "email": "ingo@fischer-ka.de" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Apollon77", 12 | "email": "ingo@fischer-ka.de" 13 | } 14 | ], 15 | "homepage": "https://github.com/Apollon77/alexa-cookie", 16 | "license": "MIT", 17 | "keywords": [ 18 | "alexa", 19 | "echo", 20 | "alexa remote", 21 | "layla.amazon.de" 22 | ], 23 | "dependencies": { 24 | "cookie": "^0.6.0", 25 | "express": "^4.21.2", 26 | "http-proxy-middleware": "^2.0.9", 27 | "http-proxy-response-rewrite": "^0.0.1", 28 | "https": "^1.0.0", 29 | "querystring": "^0.2.1" 30 | }, 31 | "devDependencies": { 32 | "@alcalzone/release-script": "^3.8.0", 33 | "@alcalzone/release-script-plugin-license": "^3.7.0", 34 | "eslint": "^8.57.1" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/Apollon77/alexa-cookie.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/Apollon77/alexa-cookie/issues" 42 | }, 43 | "scripts": { 44 | "release": "release-script" 45 | }, 46 | "engines": { 47 | "node": ">=16.0.0" 48 | }, 49 | "main": "alexa-cookie.js", 50 | "readmeFilename": "readme.md" 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # ========================= 38 | # Operating System Files 39 | # ========================= 40 | 41 | # OSX 42 | # ========================= 43 | 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | 48 | # Thumbnails 49 | ._* 50 | 51 | # Files that might appear in the root of a volume 52 | .DocumentRevisions-V100 53 | .fseventsd 54 | .Spotlight-V100 55 | .TemporaryItems 56 | .Trashes 57 | .VolumeIcon.icns 58 | 59 | # Directories potentially created on remote AFP share 60 | .AppleDB 61 | .AppleDesktop 62 | Network Trash Folder 63 | Temporary Items 64 | .apdisk 65 | 66 | # Windows 67 | # ========================= 68 | 69 | # Windows image file caches 70 | Thumbs.db 71 | ehthumbs.db 72 | 73 | # Folder config file 74 | Desktop.ini 75 | 76 | # Recycle Bin used on file shares 77 | $RECYCLE.BIN/ 78 | 79 | # Windows Installer files 80 | *.cab 81 | *.msi 82 | *.msm 83 | *.msp 84 | 85 | # Windows shortcuts 86 | *.lnk 87 | 88 | .idea 89 | 90 | lib/formerDataStore.json -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | /* jshint -W097 */ 2 | /* jshint -W030 */ 3 | /* jshint strict: false */ 4 | /* jslint node: true */ 5 | /* jslint esversion: 6 */ 6 | 7 | const alexaCookie = require('../alexa-cookie'); 8 | 9 | const config = { 10 | logger: console.log, 11 | proxyOwnIp: '...', // required if proxy enabled: provide the own IP with which you later access the proxy. 12 | // Providing/Using a hostname here can lead to issues! 13 | // Needed to set up all rewriting and proxy stuff internally 14 | 15 | // The following options are optional. Try without them first and just use really needed ones!! 16 | 17 | amazonPage: 'amazon.de', // optional: possible to use with different countries, default is 'amazon.de' 18 | acceptLanguage: 'de-DE', // optional: webpage language, should match to amazon-Page, default is 'de-DE' 19 | userAgent: '...', // optional: own userAgent to use for all request, overwrites default one, should not be needed 20 | proxyOnly: true, // optional: should only the proxy method be used? When no email/password are provided this will set to true automatically, default: false 21 | setupProxy: true, // optional: should the library setup a proxy to get cookie when automatic way did not worked? Default false! 22 | proxyPort: 3456, // optional: use this port for the proxy, default is 0 means random port is selected 23 | proxyListenBind: '0.0.0.0',// optional: set this to bind the proxy to a special IP, default is '0.0.0.0' 24 | proxyLogLevel: 'info', // optional: Loglevel of Proxy, default 'warn' 25 | baseAmazonPage: 'amazon.com', // optional: Change the Proxy Amazon Page - all "western countries" directly use amazon.com including australia! Change to amazon.co.jp for Japan 26 | amazonPageProxyLanguage: 'de_DE', // optional: language to be used for the Amazon Sign-in page the proxy calls. default is "de_DE") 27 | deviceAppName: '...', // optional: name of the device app name which will be registered with Amazon, leave empty to use a default one 28 | formerDataStorePath: '...', // optional: overwrite path where some of the formerRegistrationData are persisted to optimize against Amazon security measures 29 | formerRegistrationData: { ... }, // optional/preferred: provide the result object from subsequent proxy usages here and some generated data will be reused for next proxy call too 30 | proxyCloseWindowHTML: '...' // optional: use in order to override the default html displayed when the proxy window can be closed, default is 'Amazon Alexa Cookie successfully retrieved. You can close the browser.' 31 | }; 32 | 33 | 34 | alexaCookie.generateAlexaCookie(/*'amazon@email.de', 'amazon-password',*/ config, (err, result) => { 35 | console.log('RESULT: ' + err + ' / ' + JSON.stringify(result)); 36 | if (result && result.csrf) { 37 | alexaCookie.stopProxyServer(); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - enhancement 16 | - security 17 | - bug 18 | 19 | # Set to true to ignore issues in a project (defaults to false) 20 | exemptProjects: true 21 | 22 | # Set to true to ignore issues in a milestone (defaults to false) 23 | exemptMilestones: true 24 | 25 | # Set to true to ignore issues with an assignee (defaults to false) 26 | exemptAssignees: false 27 | 28 | # Label to use when marking as stale 29 | staleLabel: wontfix 30 | 31 | # Comment to post when marking as stale. Set to `false` to disable 32 | markComment: > 33 | This issue has been automatically marked as stale because it has not had 34 | recent activity. It will be closed if no further activity occurs within the next 7 days. 35 | Please check if the issue is still relevant in the most current version of the adapter 36 | and tell us. Also check that all relevant details, logs and reproduction steps 37 | are included and update them if needed. 38 | Thank you for your contributions. 39 | 40 | Dieses Problem wurde automatisch als veraltet markiert, da es in letzter Zeit keine Aktivitäten gab. 41 | Es wird geschlossen, wenn nicht innerhalb der nächsten 7 Tage weitere Aktivitäten stattfinden. 42 | Bitte überprüft, ob das Problem auch in der aktuellsten Version des Adapters noch relevant ist, 43 | und teilt uns dies mit. Überprüft auch, ob alle relevanten Details, Logs und Reproduktionsschritte 44 | enthalten sind bzw. aktualisiert diese. 45 | Vielen Dank für Eure Unterstützung. 46 | 47 | # Comment to post when removing the stale label. 48 | # unmarkComment: > 49 | # Your comment here. 50 | 51 | # Comment to post when closing a stale Issue or Pull Request. 52 | closeComment: > 53 | This issue has been automatically closed because of inactivity. Please open a new 54 | issue if still relevant and make sure to include all relevant details, logs and 55 | reproduction steps. 56 | Thank you for your contributions. 57 | 58 | Dieses Problem wurde aufgrund von Inaktivität automatisch geschlossen. Bitte öffnet ein 59 | neues Issue, falls dies noch relevant ist und stellt sicher das alle relevanten Details, 60 | Logs und Reproduktionsschritte enthalten sind. 61 | Vielen Dank für Eure Unterstützung. 62 | 63 | # Limit the number of actions per hour, from 1-30. Default is 30 64 | limitPerRun: 30 65 | 66 | # Limit to only `issues` or `pulls` 67 | only: issues 68 | 69 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 70 | # pulls: 71 | # daysUntilStale: 30 72 | # markComment: > 73 | # This pull request has been automatically marked as stale because it has not had 74 | # recent activity. It will be closed if no further activity occurs. Thank you 75 | # for your contributions. 76 | 77 | # issues: 78 | # exemptLabels: 79 | # - confirmed 80 | -------------------------------------------------------------------------------- /.github/workflows/test-and-release.yml: -------------------------------------------------------------------------------- 1 | # This is a composition of lint and test scripts 2 | # Make sure to update this file along with the others 3 | 4 | name: Test and Release 5 | 6 | # Run this job on all pushes and pull requests 7 | # as well as tags with a semantic version 8 | on: 9 | push: 10 | branches: 11 | - '*' 12 | tags: 13 | # normal versions 14 | - "v?[0-9]+.[0-9]+.[0-9]+" 15 | # pre-releases 16 | - "v?[0-9]+.[0-9]+.[0-9]+-**" 17 | pull_request: {} 18 | 19 | # Cancel previous PR/branch runs when a new commit is pushed 20 | concurrency: 21 | group: ${{ github.ref }} 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | # Performs quick checks before the expensive test runs 26 | check-and-lint: 27 | if: contains(github.event.head_commit.message, '[skip ci]') == false 28 | 29 | runs-on: ubuntu-latest 30 | 31 | strategy: 32 | matrix: 33 | node-version: [22.x] 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | 42 | - name: Install Dependencies 43 | run: npm ci 44 | 45 | # - name: Run local tests 46 | # run: npm test 47 | 48 | # Deploys the final package to NPM 49 | deploy: 50 | needs: [check-and-lint] 51 | 52 | # Trigger this step only when a commit on master is tagged with a version number 53 | if: | 54 | contains(github.event.head_commit.message, '[skip ci]') == false && 55 | github.event_name == 'push' && 56 | startsWith(github.ref, 'refs/tags/') 57 | runs-on: ubuntu-latest 58 | strategy: 59 | matrix: 60 | node-version: [22.x] 61 | 62 | steps: 63 | - name: Checkout code 64 | uses: actions/checkout@v4 65 | 66 | - name: Use Node.js ${{ matrix.node-version }} 67 | uses: actions/setup-node@v4 68 | with: 69 | node-version: ${{ matrix.node-version }} 70 | 71 | - name: Extract the version and commit body from the tag 72 | shell: bash 73 | id: extract_release 74 | # The body may be multiline, therefore newlines and % need to be escaped 75 | run: | 76 | VERSION="${{ github.ref }}" 77 | VERSION=${VERSION##*/v} 78 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 79 | EOF=$(od -An -N6 -x /dev/urandom | tr -d ' ') 80 | BODY=$(git show -s --format=%b) 81 | echo "BODY<<$EOF" >> $GITHUB_OUTPUT 82 | echo "$BODY" >> $GITHUB_OUTPUT 83 | echo "$EOF" >> $GITHUB_OUTPUT 84 | if [[ $VERSION == *"-"* ]] ; then 85 | echo "TAG=--tag next" >> $GITHUB_OUTPUT 86 | fi 87 | 88 | - name: Install Dependencies 89 | run: npm ci 90 | 91 | # - name: Create a clean build 92 | # run: npm run build 93 | - name: Publish package to npm 94 | run: | 95 | npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 96 | npm whoami 97 | npm publish ${{ steps.extract_release.outputs.TAG }} 98 | 99 | - name: Create Github Release 100 | uses: softprops/action-gh-release@v1 101 | env: 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | with: 104 | tag_name: ${{ github.ref }} 105 | name: Release v${{ steps.extract_release.outputs.VERSION }} 106 | draft: false 107 | # Prerelease versions create prereleases on Github 108 | prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }} 109 | body: ${{ steps.extract_release.outputs.BODY }} 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alexa-cookie 2 | 3 | [![NPM version](http://img.shields.io/npm/v/alexa-cookie2.svg)](https://www.npmjs.com/package/alexa-cookie2) 4 | [![Downloads](https://img.shields.io/npm/dm/alexa-cookie2.svg)](https://www.npmjs.com/package/alexa-cookie2) 5 | ![Test and Release](https://github.com/Apollon77/alexa-cookie/workflows/Test%20and%20Release/badge.svg) 6 | 7 | Library to generate/retrieve a cookie including a csrf for alexa remote 8 | 9 | ## Disclaimer 10 | **All product and company names or logos are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them or any associated subsidiaries! This personal project is maintained in spare time and has no business goal.** 11 | **ALEXA is a trademark of AMAZON TECHNOLOGIES, INC.** 12 | 13 | ## Description 14 | This library can be used to get the cookies needed to access Amazon Alexa services from outside. It authenticates with Amazon and gathers all needed details. These details are returned in the callback. 15 | If the automatic authentication fails (which is more common case in the meantime because of security checks from amazon like a needed Captcha or because you enabled two factor authentication) the library can also setup a proxy server to allow the manual login and will catch the cookie by itself. Using this proxy you can enter needed 2FA codes or solve captchas and still do not need to trick around to get the cookie. 16 | 17 | Starting with version 2.0 of this library the proxy approach was changed to be more "as the Amazon mobile Apps" which registers a device at Amazon and uses OAuth tokens to handle the automatic refresh of the cookies afterwards. This should work seamless. A cookie is valid for 14 days, so it is preferred to refresh the cookie after 5-13 days (please report if it should be shorter). 18 | 19 | ## Troubleshooting for getting the cookie and tokens initially 20 | If you still use the E-Mail or SMS based 2FA flow then this might not work. Please update the 2FA/OTP method in the amazon settings to the current process. 21 | 22 | If you open the Proxy URL from a mobile device where also the Alexa App is installed on it might be that it do not work because Amazon might open the Alexa App. So please use a device or PC where the Alexa App is not installed 23 | 24 | If you see a page that tells you that "alexa.amazon.xx is deprecated" and you should use the alexa app and with a QR code on it when you enter the Proxy URL" then this means that you call the proxy URL with a different IP/Domainname then the one you entered in the "proxy own IP" settings or you adjusted the IP shown in the Adapter configuration. The "proxy own IP" setting **needs to** match the IP/Domainname you use to call the proxy URL! 25 | 26 | ## Example: 27 | See example folder! 28 | 29 | * **example.js** shows how to use the library to initially get a cookie 30 | * **refresh.js** shown how to use the library to refresh the cookies 31 | 32 | ## Usage 33 | Special note for callback return for parameter result: 34 | 35 | ### When automatic cookie retrieval worked (uncommon) 36 | If the library was able to automatically log you in and get the cookie (which is the more uncommon case in the meantime) the object returned will contain keys "cookie" and "csrf" to use. 37 | 38 | ### When proxy was used (preferred and more common case) 39 | If the Proxy was used (or especially when "proxyOnly" was set in options) then result is a object with much more data. 40 | 41 | Important for the further interaction with alexa are the keys "localCookie" (same as "cookie" above) and pot. "crsf". I decided for different keys to make sure the next lines are understood by the developer ... 42 | 43 | **Please store the returned object and provide this object in all subsequent calls to the library in the options object in key "formerRegistrationData" as shown in the example!** 44 | 45 | If you not do this a new device is created each time the proxy is used which can end up in having many unused devices (such a device is like a mobile phone where you use the Alexa App with). 46 | 47 | Please use the new method "refreshAlexaCookie" to refresh the cookie data. It takes the same options object as the other method and requires the key "formerRegistrationData". It returns an updated object will all data as above. Please also store this and provide for subsequent calls! 48 | 49 | Since 4.0.0 of this library a new key called "macDms" is also returned when cookies are generated or refreshed. This is (right now Oct 2021) needed to use the Push Connection (alexa-remote library). Better strt also persisting this field, might be needed more later on. 50 | 51 | ## Thanks: 52 | A big thanks go to soef for the initial version of this library and to many other community users to support in finding out what Amazon changes here and there. 53 | 54 | Partly based on [Amazon Alexa Remote Control](http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html) (PLAIN shell) and [alexa-remote-control](https://github.com/thorsten-gehrig/alexa-remote-control) and the the Proxy idea from [OpenHab-Addon](https://github.com/openhab/openhab2-addons/blob/f54c9b85016758ff6d271b62d255bbe41a027928/addons/binding/org.openhab.binding.amazonechocontrol). Also the new way to refresh cookie and all needed changes were developed in close cooperation with @mgeramb 55 | Thank you for that work. 56 | 57 | ## Changelog: 58 | ### 5.0.3 (2025-07-13) 59 | * (Apollon77) Update some version details to sync with Alexa App 60 | 61 | ### 5.0.2 (2023-11-25) 62 | * (Apollon77) Adjust some texts 63 | 64 | ### 5.0.1 (2023-11-24) 65 | * (adn77) make registered device name configurable by Appname 66 | * (Apollon77) Prevent some error/crash cases 67 | 68 | ### 5.0.0 (2023-09-08) 69 | * IMPORTANT: Node.js 16 is now required minimum Node.js version! 70 | * (Apollon77) Enhance registration process by also registering the app capabilities to allow usage of new HTTP/2 push connection 71 | 72 | ### 4.2.0 (2023-08-08) 73 | * (Hive) Adds the ability to alter the close proxy message 74 | 75 | ### 4.1.3 (2022-08-03) 76 | * (Apollon77) Fix device registration and token exchange in other regions 77 | * (Apollon77) Use the chosen App name also for refreshing of tokens 78 | * (Apollon77) General updates 79 | 80 | ### 4.1.2 (2022-07-19) 81 | * (Apollon77) Prevent crash case 82 | 83 | ### 4.1.1 (2022-07-18) 84 | * (Apollon77/bbindreiter) Update used User-Agent for some requests 85 | 86 | ### 4.1.0 (2022-07-18) 87 | * (Apollon77) Allow to overwrite the used App-Name for the Amazon App Registration. 88 | * (Apollon77) Include the used app name also in the response 89 | 90 | ### 4.0.3 (2022-07-06) 91 | * (Apollon77) Update some request meta data to match current Alexa Apps 92 | 93 | ### 4.0.2 (2022-06-30) 94 | * (Apollon77) Prevent potential crash cases 95 | 96 | ### 4.0.1 (2021-10-11) 97 | * (Apollon77) Adjust call headers 98 | 99 | ### 4.0.0 (2021-10-11) 100 | * IMPORTANT: Node.js 10 support is dropped, supports LTS versions of Node.js starting with 12.x 101 | * (Apollon77) Add support to get macDms with relevant data from the device registration process to use in push connection. 102 | * (adn77) Update Login Flow to use match to most current Alexa App flow using code auth 103 | * (Apollon77) Update deps, drop Node.js 10 support 104 | 105 | ### 3.4.3 (2021-04-18) 106 | * (Apollon77) handle potential crash case (Sentry IOBROKER-ALEXA2-86) 107 | 108 | ### 3.4.2 (2020-11-23) 109 | * (Apollon77) handle potential crash cases (Sentry IOBROKER-ALEXA2-23, IOBROKER-ALEXA2-2B) 110 | 111 | ### 3.4.1 (2020-07-24) 112 | * (Apollon77) Try to revert one change and only use BaseHandle when .jp is on the end of the Domainname for japanese 113 | 114 | ### 3.4.0 (2020-07-19) 115 | * (Apollon77) Do not reuse device id from formerRegistrationData if store is invalid 116 | * (Apollon77) Allow to set path for former file from extern 117 | 118 | ### 3.3.3 (2020-07-16) 119 | * (Apollon77) Another try to work around Amazon changes 120 | 121 | ### 3.3.2 (2020-07-15) 122 | * (Apollon77) Another try to work around Amazon changes 123 | 124 | ### 3.3.1 (2020-07-15) 125 | * (Apollon77) Another try to work around Amazon changes 126 | 127 | ### 3.3.0 (2020-07-13) 128 | * (Apollon77) Adjust to latest Amazon changes 129 | * (Apollon77) Remember latest virtual device settings to adress amazons new security measures 130 | * (Apollon77) Adjust handling for japanese amazon region 131 | * (Apollon77) Handle error correctly when proxy server could not be initialized correctly (Sentry IOBROKER-ALEXA2-1E) 132 | * (Apollon77) handle all object changes correctly (Sentry IOBROKER-ALEXA2-1F) 133 | * (Apollon77) handle error cases better (Sentry IOBROKER-ALEXA2-1N) 134 | 135 | ### 3.2.1 (2020-06-17) 136 | * (Apollon77) another optimization for Node.js 14 137 | 138 | ### 3.2.0 (2020-06-15) 139 | * (Apollon77) Make compatible with Node.js 14 140 | * (Apollon77) Adjust to changes from Amazon so that initial Proxy process works again 141 | * (Apollon77) Add new parameter baseAmazonPage to allow use the library also for other regions (e.g. set to amazon.co.jp for japanese) 142 | 143 | ### 3.0.3 (2020.03.16) 144 | * (Apollon77) Prevent error for empty Cookie cases (on communication errors) 145 | 146 | ### 3.0.2 (2019.12.27) 147 | * (Apollon77) Prevent error when no headers are existent 148 | 149 | ### 3.0.1 (2019.12.24) 150 | * (Apollon77) Prevent error thrown when proxy port already in use 151 | 152 | ### 3.0.0 153 | * (tonesto7 / Gabriele-V) Added others CSRF after Amazon changes 154 | * (Apollon77) update deps 155 | * (Apollon77) Add GitHub Actions for check and release 156 | * (Apollon77) Rebuild a bit to allow parallel instances to work, should be compatible ... 157 | 158 | ### 2.1.0 159 | * (Apollon77) Adjust to get CSRF from different URLs after changes from Amazon 160 | 161 | ### 2.0.1 162 | * (Apollon77) Fix refresh problem, hopefully 163 | 164 | ### 2.0.0 165 | * (Apollon77) Switch Proxy approach to use device registration logic and allow refreshing of cookies. Be aware: Breaking changes in API!! 166 | 167 | ### 1.0.3 168 | * (Apollon77) try to better handle relative redirects from amazon (seen by 2FA checks) 169 | 170 | ### 1.0.2 171 | * (Apollon77) more Amazon tweaks 172 | 173 | ### 1.0.1 174 | * (Apollon77) better handle errors in automatic cookie generation 175 | 176 | ### 1.0.0 177 | * (Apollon77) handle Amazon change 178 | 179 | ### 0.2.x 180 | * (Apollon77) 0.2.2: fix encoding of special characters in email and password 181 | * (Apollon77) 0.2.1: Cleanup to prepare release 182 | * (Apollon77) 0.2.0: Add option to use a proxy to also retrieve the credentials if the automatic retrieval fails 183 | * (Apollon77) 0.2.0: Optimize automatic cookie retrieval, remove MacOS user agent again because the Linux one seems to work better 184 | 185 | ### 0.1.x 186 | * (Apollon77) 0.1.3: Use specific User-Agents for Win32, MacOS and linux based platforms 187 | * (Apollon77) 0.1.2: Log the used user-Agent, Accept-Language and Login-URL 188 | * (Apollon77) 0.1.1: update to get it working again and sync to [alexa-remote-control](https://github.com/thorsten-gehrig/alexa-remote-control) 189 | 190 | ### 0.0.x 191 | * Versions by soef 192 | -------------------------------------------------------------------------------- /lib/proxy.js: -------------------------------------------------------------------------------- 1 | /* jshint -W097 */ 2 | /* jshint -W030 */ 3 | /* jshint strict: false */ 4 | /* jslint node: true */ 5 | /* jslint esversion: 6 */ 6 | 'use strict'; 7 | 8 | const modifyResponse = require('http-proxy-response-rewrite'); 9 | const express = require('express'); 10 | const proxy = require('http-proxy-middleware').createProxyMiddleware; 11 | const querystring = require('querystring'); 12 | const cookieTools = require('cookie'); 13 | const fs = require('fs'); 14 | const path = require('path'); 15 | const crypto = require('crypto'); 16 | 17 | const FORMERDATA_STORE_VERSION = 4; 18 | 19 | function addCookies(Cookie, headers) { 20 | if (!headers || !headers['set-cookie']) return Cookie; 21 | const cookies = cookieTools.parse(Cookie); 22 | for (let cookie of headers['set-cookie']) { 23 | cookie = cookie.match(/^([^=]+)=([^;]+);.*/); 24 | if (cookie && cookie.length === 3) { 25 | if (cookie[1] === 'ap-fid' && cookie[2] === '""') continue; 26 | cookies[cookie[1]] = cookie[2]; 27 | } 28 | } 29 | Cookie = ''; 30 | for (const name of Object.keys(cookies)) { 31 | Cookie += `${name}=${cookies[name]}; `; 32 | } 33 | Cookie = Cookie.replace(/[; ]*$/, ''); 34 | return Cookie; 35 | } 36 | 37 | function customStringify(v, func, intent) { 38 | const cache = new Map(); 39 | return JSON.stringify(v, function (key, value) { 40 | if (typeof value === 'object' && value !== null) { 41 | if (cache.get(value)) { 42 | // Circular reference found, discard key 43 | return; 44 | } 45 | // Store value in our map 46 | cache.set(value, true); 47 | } 48 | if (Buffer.isBuffer(value)) { 49 | // Buffers not relevant to be logged, ignore 50 | return; 51 | } 52 | return value; 53 | }, intent); 54 | } 55 | 56 | function initAmazonProxy(_options, callbackCookie, callbackListening) { 57 | const initialCookies = {}; 58 | 59 | const formerDataStorePath = _options.formerDataStorePath || path.join(__dirname, 'formerDataStore.json'); 60 | let formerDataStoreValid = false; 61 | if (!_options.formerRegistrationData) { 62 | try { 63 | if (fs.existsSync(formerDataStorePath)) { 64 | const formerDataStore = JSON.parse(fs.readFileSync(path.join(__dirname, 'formerDataStore.json'), 'utf8')); 65 | if (typeof formerDataStore === 'object' && formerDataStore.storeVersion === FORMERDATA_STORE_VERSION) { 66 | _options.formerRegistrationData = _options.formerRegistrationData || {}; 67 | _options.formerRegistrationData.frc = _options.formerRegistrationData.frc || formerDataStore.frc; 68 | _options.formerRegistrationData['map-md'] = _options.formerRegistrationData['map-md'] || formerDataStore['map-md']; 69 | _options.formerRegistrationData.deviceId = _options.formerRegistrationData.deviceId || formerDataStore.deviceId; 70 | _options.logger && _options.logger('Proxy Init: loaded temp data store ass fallback former data'); 71 | formerDataStoreValid = true; 72 | } 73 | } 74 | } catch (_err) { 75 | // ignore 76 | } 77 | } 78 | 79 | if (!_options.formerRegistrationData || !_options.formerRegistrationData.frc) { 80 | // frc contains 313 random bytes, encoded as base64 81 | const frcBuffer = Buffer.alloc(313); 82 | for (let i = 0; i < 313; i++) { 83 | frcBuffer.writeUInt8(Math.floor(Math.random() * 255), i); 84 | } 85 | initialCookies.frc = frcBuffer.toString('base64'); 86 | } 87 | else { 88 | _options.logger && _options.logger('Proxy Init: reuse frc from former data'); 89 | initialCookies.frc = _options.formerRegistrationData.frc; 90 | } 91 | 92 | if (!_options.formerRegistrationData || !_options.formerRegistrationData['map-md']) { 93 | initialCookies['map-md'] = Buffer.from('{"device_user_dictionary":[],"device_registration_data":{"software_version":"1"},"app_identifier":{"app_version":"2.2.485407","bundle_id":"com.amazon.echo"}}').toString('base64'); 94 | } 95 | else { 96 | _options.logger && _options.logger('Proxy Init: reuse map-md from former data'); 97 | initialCookies['map-md'] = _options.formerRegistrationData['map-md']; 98 | } 99 | 100 | let deviceId = ''; 101 | if (!_options.formerRegistrationData || !_options.formerRegistrationData.deviceId || !formerDataStoreValid) { 102 | const buf = Buffer.alloc(16); // 16 random bytes 103 | const bufHex = crypto.randomFillSync(buf).toString('hex').toUpperCase(); // convert into hex = 32x 0-9A-F 104 | deviceId = Buffer.from(bufHex).toString('hex'); // convert into hex = 64 chars that are hex of hex id 105 | deviceId += '23413249564c5635564d32573831'; 106 | } 107 | else { 108 | _options.logger && _options.logger('Proxy Init: reuse deviceId from former data'); 109 | deviceId = _options.formerRegistrationData.deviceId; 110 | } 111 | 112 | try { 113 | const formerDataStore = { 114 | 'storeVersion': FORMERDATA_STORE_VERSION, 115 | 'deviceId': deviceId, 116 | 'map-md': initialCookies['map-md'], 117 | 'frc': initialCookies.frc 118 | }; 119 | fs.writeFileSync(formerDataStorePath, JSON.stringify(formerDataStore), 'utf8'); 120 | } 121 | catch (_err) { 122 | // ignore 123 | } 124 | 125 | function base64URLEncode(str) { 126 | return str.toString('base64') 127 | .replace(/\+/g, '-') 128 | .replace(/\//g, '_') 129 | .replace(/=/g, ''); 130 | } 131 | function sha256(buffer) { 132 | return crypto.createHash('sha256').update(buffer).digest(); 133 | } 134 | const code_verifier = base64URLEncode(crypto.randomBytes(32)); 135 | const code_challenge = base64URLEncode(sha256(code_verifier)); 136 | 137 | let proxyCookies = ''; 138 | 139 | // proxy middleware options 140 | const optionsAlexa = { 141 | target: `https://alexa.${_options.baseAmazonPage}`, 142 | changeOrigin: true, 143 | ws: false, 144 | pathRewrite: {}, // enhanced below 145 | router: router, 146 | hostRewrite: true, 147 | followRedirects: false, 148 | logLevel: _options.proxyLogLevel, 149 | onError: onError, 150 | onProxyRes: onProxyRes, 151 | onProxyReq: onProxyReq, 152 | headers: { 153 | 'user-agent': 'AppleWebKit PitanguiBridge/2.2.485407.0-[HARDWARE=iPhone10_4][SOFTWARE=15.5][DEVICE=iPhone]', 154 | 'accept-language': _options.acceptLanguage, 155 | 'authority': `www.${_options.baseAmazonPage}` 156 | }, 157 | cookieDomainRewrite: { // enhanced below 158 | '*': '' 159 | } 160 | }; 161 | optionsAlexa.pathRewrite[`^/www.${_options.baseAmazonPage}`] = ''; 162 | optionsAlexa.pathRewrite[`^/alexa.${_options.baseAmazonPage}`] = ''; 163 | optionsAlexa.cookieDomainRewrite[`.${_options.baseAmazonPage}`] = _options.proxyOwnIp; 164 | optionsAlexa.cookieDomainRewrite[_options.baseAmazonPage] = _options.proxyOwnIp; 165 | if (_options.logger) optionsAlexa.logProvider = function logProvider() { 166 | return { 167 | log: _options.logger.log || _options.logger, 168 | debug: _options.logger.debug || _options.logger, 169 | info: _options.logger.info || _options.logger, 170 | warn: _options.logger.warn || _options.logger, 171 | error: _options.logger.error || _options.logger 172 | }; 173 | }; 174 | let returnedInitUrl; 175 | 176 | function router(req) { 177 | const url = (req.originalUrl || req.url); 178 | _options.logger && _options.logger(`Router: ${url} / ${req.method} / ${JSON.stringify(req.headers)}`); 179 | if (req.headers.host === `${_options.proxyOwnIp}:${_options.proxyPort}`) { 180 | if (url.startsWith(`/www.${_options.baseAmazonPage}/`)) { 181 | return `https://www.${_options.baseAmazonPage}`; 182 | } else if (url.startsWith(`/alexa.${_options.baseAmazonPage}/`)) { 183 | return `https://alexa.${_options.baseAmazonPage}`; 184 | } else if (req.headers.referer) { 185 | if (req.headers.referer.startsWith(`http://${_options.proxyOwnIp}:${_options.proxyPort}/www.${_options.baseAmazonPage}/`)) { 186 | return `https://www.${_options.baseAmazonPage}`; 187 | } else if (req.headers.referer.startsWith(`http://${_options.proxyOwnIp}:${_options.proxyPort}/alexa.${_options.baseAmazonPage}/`)) { 188 | return `https://alexa.${_options.baseAmazonPage}`; 189 | } 190 | } 191 | if (url === '/') { // initial redirect 192 | returnedInitUrl = `https://www.${_options.baseAmazonPage}/ap/signin?openid.return_to=https%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fmaplanding&openid.assoc_handle=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&pageId=amzn_dp_project_dee_ios${_options.baseAmazonPageHandle}&accountStatusPolicy=P1&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns.oa2=http%3A%2F%2Fwww.${_options.baseAmazonPage}%2Fap%2Fext%2Foauth%2F2&openid.oa2.client_id=device%3A${deviceId}&openid.ns.pape=http%3A%2F%2Fspecs.openid.net%2Fextensions%2Fpape%2F1.0&openid.oa2.response_type=code&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.pape.max_auth_age=0&openid.oa2.scope=device_auth_access&openid.oa2.code_challenge_method=S256&openid.oa2.code_challenge=${code_challenge}&language=${_options.amazonPageProxyLanguage}`; 193 | _options.logger && _options.logger(`Alexa-Cookie: Initial Page Request: ${returnedInitUrl}`); 194 | return returnedInitUrl; 195 | } 196 | else { 197 | return `https://www.${_options.baseAmazonPage}`; 198 | } 199 | } 200 | return `https://alexa.${_options.baseAmazonPage}`; 201 | } 202 | 203 | function onError(err, req, res) { 204 | _options.logger && _options.logger(`ERROR: ${err}`); 205 | try { 206 | res.writeHead(500, { 207 | 'Content-Type': 'text/plain' 208 | }); 209 | res.end(`Proxy-Error: ${err}`); 210 | } catch (err) { 211 | // ignore 212 | } 213 | } 214 | 215 | function replaceHosts(data) { 216 | //const dataOrig = data; 217 | const amazonRegex = new RegExp(`https?://www.${_options.baseAmazonPage}:?[0-9]*/`.replace(/\./g, '\\.'), 'g'); 218 | const alexaRegex = new RegExp(`https?://alexa.${_options.baseAmazonPage}:?[0-9]*/`.replace(/\./g, '\\.'), 'g'); 219 | data = data.replace(///g, '/'); 220 | data = data.replace(amazonRegex, `http://${_options.proxyOwnIp}:${_options.proxyPort}/www.${_options.baseAmazonPage}/`); 221 | data = data.replace(alexaRegex, `http://${_options.proxyOwnIp}:${_options.proxyPort}/alexa.${_options.baseAmazonPage}/`); 222 | //_options.logger && _options.logger('REPLACEHOSTS: ' + dataOrig + ' --> ' + data); 223 | return data; 224 | } 225 | 226 | function replaceHostsBack(data) { 227 | const amazonRegex = new RegExp(`http://${_options.proxyOwnIp}:${_options.proxyPort}/www.${_options.baseAmazonPage}/`.replace(/\./g, '\\.'), 'g'); 228 | const alexaRegex = new RegExp(`http://${_options.proxyOwnIp}:${_options.proxyPort}/alexa.${_options.baseAmazonPage}/`.replace(/\./g, '\\.'), 'g'); 229 | data = data.replace(amazonRegex, `https://www.${_options.baseAmazonPage}/`); 230 | data = data.replace(alexaRegex, `https://alexa.${_options.baseAmazonPage}/`); 231 | if (data === `http://${_options.proxyOwnIp}:${_options.proxyPort}/`) { 232 | data = returnedInitUrl; 233 | } 234 | return data; 235 | } 236 | 237 | function onProxyReq(proxyReq, req/*, _res*/) { 238 | const url = req.originalUrl || req.url; 239 | if (url.endsWith('.ico') || url.endsWith('.js') || url.endsWith('.ttf') || url.endsWith('.svg') || url.endsWith('.png') || url.endsWith('.appcache')) return; 240 | //if (url.startsWith('/ap/uedata')) return; 241 | 242 | _options.logger && _options.logger(`Alexa-Cookie: Proxy-Request: ${req.method} ${url}`); 243 | //_options.logger && _options.logger('Alexa-Cookie: Proxy-Request-Data: ' + customStringify(proxyReq, null, 2)); 244 | 245 | if (typeof proxyReq.getHeader === 'function') { 246 | _options.logger && _options.logger(`Alexa-Cookie: Headers: ${JSON.stringify(proxyReq.getHeaders())}`); 247 | let reqCookie = proxyReq.getHeader('cookie'); 248 | if (reqCookie === undefined) { 249 | reqCookie = ''; 250 | } 251 | for (const cookie of Object.keys(initialCookies)) { 252 | if (!reqCookie.includes(`${cookie}=`)) { 253 | reqCookie += `; ${cookie}=${initialCookies[cookie]}`; 254 | } 255 | } 256 | if (reqCookie.startsWith('; ')) { 257 | reqCookie = reqCookie.substr(2); 258 | } 259 | proxyReq.setHeader('cookie', reqCookie); 260 | if (!proxyCookies.length) { 261 | proxyCookies = reqCookie; 262 | } else { 263 | proxyCookies += `; ${reqCookie}`; 264 | } 265 | _options.logger && _options.logger(`Alexa-Cookie: Headers: ${JSON.stringify(proxyReq.getHeaders())}`); 266 | } 267 | 268 | let modified = false; 269 | if (req.method === 'POST') { 270 | if (typeof proxyReq.getHeader === 'function' && proxyReq.getHeader('referer')) { 271 | const fixedReferer = replaceHostsBack(proxyReq.getHeader('referer')); 272 | if (fixedReferer ) { 273 | proxyReq.setHeader('referer', fixedReferer); 274 | _options.logger && _options.logger(`Alexa-Cookie: Modify headers: Changed Referer: ${fixedReferer}`); 275 | modified = true; 276 | } 277 | } 278 | if (typeof proxyReq.getHeader === 'function' && proxyReq.getHeader('origin') !== `https://${proxyReq.getHeader('host')}`) { 279 | proxyReq.setHeader('origin', `https://www.${_options.baseAmazonPage}`); 280 | _options.logger && _options.logger('Alexa-Cookie: Modify headers: Delete Origin'); 281 | modified = true; 282 | } 283 | 284 | let postBody = ''; 285 | req.on('data', chunk => { 286 | postBody += chunk.toString(); // convert Buffer to string 287 | }); 288 | } 289 | _options.proxyLogLevel === 'debug' && _options.logger && _options.logger(`Alexa-Cookie: Proxy-Request: (modified:${modified})${customStringify(proxyReq, null, 2)}`); 290 | } 291 | 292 | function onProxyRes(proxyRes, req, res) { 293 | const url = req.originalUrl || req.url; 294 | if (url.endsWith('.ico') || url.endsWith('.js') || url.endsWith('.ttf') || url.endsWith('.svg') || url.endsWith('.png') || url.endsWith('.appcache')) return; 295 | if (url.startsWith('/ap/uedata')) return; 296 | //_options.logger && _options.logger('Proxy-Response: ' + customStringify(proxyRes, null, 2)); 297 | let reqestHost = null; 298 | if (proxyRes.socket && proxyRes.socket._host) reqestHost = proxyRes.socket._host; 299 | _options.logger && _options.logger(`Alexa-Cookie: Proxy Response from Host: ${reqestHost}`); 300 | _options.proxyLogLevel === 'debug' && _options.logger && _options.logger(`Alexa-Cookie: Proxy-Response Headers: ${customStringify(proxyRes.headers, null, 2)}`); 301 | _options.proxyLogLevel === 'debug' && _options.logger && _options.logger(`Alexa-Cookie: Proxy-Response Outgoing: ${customStringify(proxyRes.socket.parser.outgoing, null, 2)}`); 302 | //_options.logger && _options.logger('Proxy-Response RES!!: ' + customStringify(res, null, 2)); 303 | 304 | if (proxyRes && proxyRes.headers && proxyRes.headers['set-cookie']) { 305 | // make sure cookies are also sent to http by remove secure flags 306 | for (let i = 0; i < proxyRes.headers['set-cookie'].length; i++) { 307 | proxyRes.headers['set-cookie'][i] = proxyRes.headers['set-cookie'][i].replace('Secure', ''); 308 | } 309 | proxyCookies = addCookies(proxyCookies, proxyRes.headers); 310 | } 311 | _options.logger && _options.logger(`Alexa-Cookie: Cookies handled: ${JSON.stringify(proxyCookies)}`); 312 | 313 | if ( 314 | (proxyRes.socket && proxyRes.socket._host === `www.${_options.baseAmazonPage}` && proxyRes.socket.parser.outgoing && proxyRes.socket.parser.outgoing.method === 'GET' && proxyRes.socket.parser.outgoing.path.startsWith('/ap/maplanding')) || 315 | (proxyRes.socket && proxyRes.socket.parser.outgoing && proxyRes.socket.parser.outgoing.getHeader('location') && proxyRes.socket.parser.outgoing.getHeader('location').includes('/ap/maplanding?')) || 316 | (proxyRes.headers.location && (proxyRes.headers.location.includes('/ap/maplanding?') || proxyRes.headers.location.includes('/spa/index.html'))) 317 | ) { 318 | _options.logger && _options.logger('Alexa-Cookie: Proxy detected SUCCESS!!'); 319 | 320 | const paramStart = proxyRes.headers.location.indexOf('?'); 321 | const queryParams = querystring.parse(proxyRes.headers.location.substr(paramStart + 1)); 322 | 323 | proxyRes.statusCode = 302; 324 | proxyRes.headers.location = `http://${_options.proxyOwnIp}:${_options.proxyPort}/cookie-success`; 325 | delete proxyRes.headers.referer; 326 | 327 | _options.logger && _options.logger(`Alexa-Cookie: Proxy catched cookie: ${proxyCookies}`); 328 | _options.logger && _options.logger(`Alexa-Cookie: Proxy catched parameters: ${JSON.stringify(queryParams)}`); 329 | 330 | callbackCookie && callbackCookie(null, { 331 | 'loginCookie': proxyCookies, 332 | 'authorization_code': queryParams['openid.oa2.authorization_code'], 333 | 'frc': initialCookies.frc, 334 | 'map-md': initialCookies['map-md'], 335 | 'deviceId': deviceId, 336 | 'verifier': code_verifier 337 | }); 338 | return; 339 | } 340 | 341 | // If we detect a redirect, rewrite the location header 342 | if (proxyRes.headers.location) { 343 | _options.logger && _options.logger(`Redirect: Original Location ----> ${proxyRes.headers.location}`); 344 | proxyRes.headers.location = replaceHosts(proxyRes.headers.location); 345 | if (reqestHost && proxyRes.headers.location.startsWith('/')) { 346 | proxyRes.headers.location = `http://${_options.proxyOwnIp}:${_options.proxyPort}/${reqestHost}${proxyRes.headers.location}`; 347 | } 348 | _options.logger && _options.logger(`Redirect: Final Location ----> ${proxyRes.headers.location}`); 349 | return; 350 | } 351 | 352 | modifyResponse(res, (proxyRes && proxyRes.headers ? proxyRes.headers['content-encoding'] || '' : ''), function(body) { 353 | if (body) { 354 | const bodyOrig = body; 355 | body = replaceHosts(body); 356 | if (body !== bodyOrig) { 357 | _options.logger && _options.logger('Alexa-Cookie: MODIFIED Response Body to rewrite URLs'); 358 | _options.logger && _options.logger(''); 359 | _options.logger && _options.logger(''); 360 | _options.logger && _options.logger(''); 361 | } 362 | } 363 | return body; 364 | }); 365 | } 366 | 367 | // create the proxy (without context) 368 | const myProxy = proxy('!/cookie-success', optionsAlexa); 369 | 370 | // mount `exampleProxy` in web server 371 | const app = express(); 372 | 373 | app.use(myProxy); 374 | app.get('/cookie-success', function(req, res) { 375 | res.send(_options.proxyCloseWindowHTML); 376 | }); 377 | if (_options.proxyPort< 1 || _options.proxyPort > 65535) { 378 | _options.logger && _options.logger(`Alexa-Cookie: Error: Port ${_options.proxyPort} invalid. Use random port.`); 379 | _options.proxyPort = undefined; 380 | } 381 | const server = app.listen(_options.proxyPort, _options.proxyListenBind, function() { 382 | _options.logger && _options.logger(`Alexa-Cookie: Proxy-Server listening on port ${server.address().port}`); 383 | callbackListening && callbackListening(server); 384 | callbackListening = null; 385 | }).on('error', err => { 386 | _options.logger && _options.logger(`Alexa-Cookie: Proxy-Server Error: ${err}`); 387 | callbackListening && callbackListening(null); 388 | callbackListening = null; 389 | }); 390 | 391 | } 392 | 393 | module.exports.initAmazonProxy = initAmazonProxy; 394 | -------------------------------------------------------------------------------- /alexa-cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * partly based on Amazon Alexa Remote Control (PLAIN shell) 3 | * http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html AND on 4 | * https://github.com/thorsten-gehrig/alexa-remote-control 5 | * and much enhanced ... 6 | */ 7 | 8 | const https = require('https'); 9 | const querystring = require('querystring'); 10 | const url = require('url'); 11 | const os = require('os'); 12 | const cookieTools = require('cookie'); 13 | const amazonProxy = require('./lib/proxy.js'); 14 | 15 | const defaultAmazonPage = 'amazon.de'; 16 | const defaultUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'; 17 | const defaultUserAgentLinux = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'; 18 | //const defaultUserAgentMacOs = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36'; 19 | const defaultProxyCloseWindowHTML = 'Amazon Alexa Cookie successfully retrieved. You can close the browser.'; 20 | const defaultAcceptLanguage = 'de-DE'; 21 | 22 | const apiCallVersion = '2.2.651540.0'; 23 | const apiCallUserAgent = 'AmazonWebView/Amazon Alexa/2.2.651540.0/iOS/18.3.1/iPhone'; 24 | const defaultAppName = 'ioBroker Alexa2'; 25 | 26 | const csrfOptions = [ 27 | '/api/language', 28 | '/spa/index.html', 29 | '/api/devices-v2/device?cached=false', 30 | '/templates/oobe/d-device-pick.handlebars', 31 | '/api/strings' 32 | ]; 33 | 34 | function AlexaCookie() { 35 | if (!(this instanceof AlexaCookie)) return new AlexaCookie(); 36 | 37 | let proxyServer; 38 | let _options; 39 | 40 | let Cookie = ''; 41 | 42 | const addCookies = (Cookie, headers) => { 43 | if (!headers || !headers['set-cookie']) return Cookie; 44 | const cookies = cookieTools.parse(Cookie || ''); 45 | for (let cookie of headers['set-cookie']) { 46 | cookie = cookie.match(/^([^=]+)=([^;]+);.*/); 47 | if (cookie && cookie.length === 3) { 48 | if (cookie[1] === 'ap-fid' && cookie[2] === '""') continue; 49 | if (cookies[cookie[1]] && cookies[cookie[1]] !== cookie[2]) { 50 | _options.logger && _options.logger(`Alexa-Cookie: Update Cookie ${cookie[1]} = ${cookie[2]}`); 51 | } else if (!cookies[cookie[1]]) { 52 | _options.logger && _options.logger(`Alexa-Cookie: Add Cookie ${cookie[1]} = ${cookie[2]}`); 53 | } 54 | cookies[cookie[1]] = cookie[2]; 55 | } 56 | } 57 | Cookie = ''; 58 | for (const name of Object.keys(cookies)) { 59 | Cookie += `${name}=${cookies[name]}; `; 60 | } 61 | Cookie = Cookie.replace(/[; ]*$/, ''); 62 | return Cookie; 63 | }; 64 | 65 | const request = (options, info, callback) => { 66 | _options.logger && _options.logger(`Alexa-Cookie: Sending Request with ${JSON.stringify(options)}`); 67 | if (typeof info === 'function') { 68 | callback = info; 69 | info = { 70 | requests: [] 71 | }; 72 | } 73 | 74 | let removeContentLength; 75 | if (options.headers && options.headers['Content-Length']) { 76 | if (!options.body) delete options.headers['Content-Length']; 77 | } else if (options.body) { 78 | if (!options.headers) options.headers = {}; 79 | options.headers['Content-Length'] = options.body.length; 80 | removeContentLength = true; 81 | } 82 | 83 | const req = https.request(options, (res) => { 84 | let body = ''; 85 | info.requests.push({options: options, response: res}); 86 | 87 | if (options.followRedirects !== false && res.statusCode >= 300 && res.statusCode < 400) { 88 | _options.logger && _options.logger(`Alexa-Cookie: Response (${res.statusCode})${res.headers.location ? ` - Redirect to ${res.headers.location}` : ''}`); 89 | //options.url = res.headers.location; 90 | const u = url.parse(res.headers.location); 91 | if (u.host) options.host = u.host; 92 | options.path = u.path; 93 | options.method = 'GET'; 94 | options.body = ''; 95 | options.headers.Cookie = Cookie = addCookies(Cookie, res.headers); 96 | 97 | res.socket && res.socket.end(); 98 | return request(options, info, callback); 99 | } else { 100 | _options.logger && _options.logger(`Alexa-Cookie: Response (${res.statusCode})`); 101 | res.on('data', (chunk) => { 102 | body += chunk; 103 | }); 104 | 105 | res.on('end', () => { 106 | if (removeContentLength) delete options.headers['Content-Length']; 107 | res.socket && res.socket.end(); 108 | callback && callback(0, res, body, info); 109 | }); 110 | } 111 | }); 112 | 113 | req.on('error', (e) => { 114 | if (typeof callback === 'function' && callback.length >= 2) { 115 | return callback(e, null, null, info); 116 | } 117 | }); 118 | if (options && options.body) { 119 | req.write(options.body); 120 | } 121 | req.end(); 122 | }; 123 | 124 | const getFields = body => { 125 | body = body.replace(/[\n\r]/g, ' '); 126 | let re = /^.*?("hidden"\s*name=".*$)/; 127 | const ar = re.exec(body); 128 | if (!ar || ar.length < 2) return {}; 129 | let h; 130 | re = /.*?name="([^"]+)"[\s^\s]*value="([^"]+).*?"/g; 131 | const data = {}; 132 | while ((h = re.exec(ar[1])) !== null) { 133 | if (h[1] !== 'rememberMe') { 134 | data[h[1]] = h[2]; 135 | } 136 | } 137 | return data; 138 | }; 139 | 140 | const initConfig = () => { 141 | _options.amazonPage = _options.amazonPage || defaultAmazonPage; 142 | if (_options.formerRegistrationData && _options.formerRegistrationData.amazonPage) _options.amazonPage = _options.formerRegistrationData.amazonPage; 143 | 144 | _options.logger && _options.logger(`Alexa-Cookie: Use as Login-Amazon-URL: ${_options.amazonPage}`); 145 | 146 | _options.baseAmazonPage = _options.baseAmazonPage || 'amazon.com'; 147 | _options.logger && _options.logger(`Alexa-Cookie: Use as Base-Amazon-URL: ${_options.baseAmazonPage}`); 148 | 149 | _options.deviceAppName = _options.deviceAppName || defaultAppName; 150 | _options.logger && _options.logger(`Alexa-Cookie: Use as Device-App-Name: ${_options.deviceAppName}`); 151 | 152 | if (!_options.baseAmazonPageHandle && _options.baseAmazonPageHandle !== '') { 153 | const amazonDomain = _options.baseAmazonPage.substr(_options.baseAmazonPage.lastIndexOf('.') + 1); 154 | if (amazonDomain === 'jp') { 155 | _options.baseAmazonPageHandle = `_${amazonDomain}`; 156 | } 157 | else if (amazonDomain !== 'com') { 158 | //_options.baseAmazonPageHandle = '_' + amazonDomain; 159 | _options.baseAmazonPageHandle = ''; 160 | } 161 | else { 162 | _options.baseAmazonPageHandle = ''; 163 | } 164 | } 165 | 166 | if (!_options.userAgent) { 167 | const platform = os.platform(); 168 | if (platform === 'win32') { 169 | _options.userAgent = defaultUserAgent; 170 | } 171 | /*else if (platform === 'darwin') { 172 | _options.userAgent = defaultUserAgentMacOs; 173 | }*/ 174 | else { 175 | _options.userAgent = defaultUserAgentLinux; 176 | } 177 | } 178 | _options.logger && _options.logger(`Alexa-Cookie: Use as User-Agent: ${_options.userAgent}`); 179 | 180 | _options.acceptLanguage = _options.acceptLanguage || defaultAcceptLanguage; 181 | 182 | _options.logger && _options.logger(`Alexa-Cookie: Use as Accept-Language: ${_options.acceptLanguage}`); 183 | 184 | _options.proxyCloseWindowHTML = _options.proxyCloseWindowHTML || defaultProxyCloseWindowHTML; 185 | 186 | if (_options.setupProxy && !_options.proxyOwnIp) { 187 | _options.logger && _options.logger('Alexa-Cookie: Own-IP Setting missing for Proxy. Disabling!'); 188 | _options.setupProxy = false; 189 | } 190 | if (_options.setupProxy) { 191 | _options.setupProxy = true; 192 | _options.proxyPort = _options.proxyPort || 0; 193 | _options.proxyListenBind = _options.proxyListenBind || '0.0.0.0'; 194 | _options.logger && _options.logger(`Alexa-Cookie: Proxy-Mode enabled if needed: ${_options.proxyOwnIp}:${_options.proxyPort} to listen on ${_options.proxyListenBind}`); 195 | } else { 196 | _options.setupProxy = false; 197 | _options.logger && _options.logger('Alexa-Cookie: Proxy mode disabled'); 198 | } 199 | _options.proxyLogLevel = _options.proxyLogLevel || 'warn'; 200 | _options.amazonPageProxyLanguage = _options.amazonPageProxyLanguage || 'de_DE'; 201 | 202 | if (_options.formerRegistrationData) _options.proxyOnly = true; 203 | }; 204 | 205 | const getCSRFFromCookies = (cookie, _options, callback) => { 206 | // get CSRF 207 | const csrfUrls = csrfOptions; 208 | 209 | function csrfTry() { 210 | const path = csrfUrls.shift(); 211 | const options = { 212 | 'host': `alexa.${_options.amazonPage}`, 213 | 'path': path, 214 | 'method': 'GET', 215 | 'headers': { 216 | 'DNT': '1', 217 | 'User-Agent': _options.userAgent, 218 | 'Connection': 'keep-alive', 219 | 'Referer': `https://alexa.${_options.amazonPage}/spa/index.html`, 220 | 'Cookie': cookie, 221 | 'Accept': '*/*', 222 | 'Origin': `https://alexa.${_options.amazonPage}` 223 | } 224 | }; 225 | 226 | _options.logger && _options.logger(`Alexa-Cookie: Step 4: get CSRF via ${path}`); 227 | request(options, (error, response) => { 228 | cookie = addCookies(cookie, response ? response.headers : null); 229 | const ar = /csrf=([^;]+)/.exec(cookie); 230 | const csrf = ar ? ar[1] : undefined; 231 | _options.logger && _options.logger(`Alexa-Cookie: Result: csrf=${csrf}, Cookie=${cookie}`); 232 | if (!csrf && csrfUrls.length) { 233 | csrfTry(); 234 | return; 235 | } 236 | callback && callback(null, { 237 | cookie: cookie, 238 | csrf: csrf 239 | }); 240 | }); 241 | } 242 | 243 | csrfTry(); 244 | }; 245 | 246 | this.generateAlexaCookie = (email, password, __options, callback) => { 247 | if (email !== undefined && typeof email !== 'string') { 248 | callback = __options; 249 | __options = password; 250 | password = email; 251 | email = null; 252 | } 253 | if (password !== undefined && typeof password !== 'string') { 254 | callback = __options; 255 | __options = password; 256 | password = null; 257 | } 258 | 259 | if (typeof __options === 'function') { 260 | callback = __options; 261 | __options = {}; 262 | } 263 | 264 | _options = __options; 265 | 266 | if (!email || !password) { 267 | __options.proxyOnly = true; 268 | } 269 | 270 | initConfig(); 271 | 272 | if (!_options.proxyOnly) { 273 | // get first cookie and write redirection target into referer 274 | const options = { 275 | host: `alexa.${_options.amazonPage}`, 276 | path: '', 277 | method: 'GET', 278 | headers: { 279 | 'DNT': '1', 280 | 'Upgrade-Insecure-Requests': '1', 281 | 'User-Agent': _options.userAgent, 282 | 'Accept-Language': _options.acceptLanguage, 283 | 'Connection': 'keep-alive', 284 | 'Accept': '*/*' 285 | }, 286 | }; 287 | _options.logger && _options.logger('Alexa-Cookie: Step 1: get first cookie and authentication redirect'); 288 | request(options, (error, response, body, info) => { 289 | if (error) { 290 | callback && callback(error, null); 291 | return; 292 | } 293 | 294 | const lastRequestOptions = info.requests[info.requests.length - 1].options; 295 | // login empty to generate session 296 | Cookie = addCookies(Cookie, response.headers); 297 | const options = { 298 | host: `www.${_options.amazonPage}`, 299 | path: '/ap/signin', 300 | method: 'POST', 301 | headers: { 302 | 'DNT': '1', 303 | 'Upgrade-Insecure-Requests': '1', 304 | 'User-Agent': _options.userAgent, 305 | 'Accept-Language': _options.acceptLanguage, 306 | 'Connection': 'keep-alive', 307 | 'Content-Type': 'application/x-www-form-urlencoded', 308 | 'Referer': `https://${lastRequestOptions.host}${lastRequestOptions.path}`, 309 | 'Cookie': Cookie, 310 | 'Accept': '*/*' 311 | }, 312 | gzip: true, 313 | body: querystring.stringify(getFields(body)) 314 | }; 315 | _options.logger && _options.logger('Alexa-Cookie: Step 2: login empty to generate session'); 316 | request(options, (error, response, body) => { 317 | if (error) { 318 | callback && callback(error, null); 319 | return; 320 | } 321 | 322 | // login with filled out form 323 | // !!! referer now contains session in URL 324 | options.host = `www.${_options.amazonPage}`; 325 | options.path = '/ap/signin'; 326 | options.method = 'POST'; 327 | options.headers.Cookie = Cookie = addCookies(Cookie, response.headers); 328 | const ar = options.headers.Cookie.match(/session-id=([^;]+)/); 329 | options.headers.Referer = `https://www.${_options.amazonPage}/ap/signin/${ar[1]}`; 330 | options.body = getFields(body); 331 | options.body.email = email || ''; 332 | options.body.password = password || ''; 333 | options.body = querystring.stringify(options.body, null, null, {encodeURIComponent: encodeURIComponent}); 334 | 335 | _options.logger && _options.logger('Alexa-Cookie: Step 3: login with filled form, referer contains session id'); 336 | request(options, (error, response, body, info) => { 337 | if (error) { 338 | callback && callback(error, null); 339 | return; 340 | } 341 | 342 | const lastRequestOptions = info.requests[info.requests.length - 1].options; 343 | 344 | // check whether the login has been successful or exit otherwise 345 | if (!lastRequestOptions.host.startsWith('alexa') || !lastRequestOptions.path.endsWith('.html')) { 346 | let errMessage = 'Login unsuccessfull. Please check credentials.'; 347 | const amazonMessage = body.match(/auth-warning-message-box[\S\s]*"a-alert-heading">([^<]*)[\S\s]*
  • <[^>]*>\s*([^<\n]*)\s* { 357 | if (!server) { 358 | return callback && callback(new Error('Proxy could not be initialized'), null); 359 | } 360 | proxyServer = server; 361 | if (!_options.proxyPort || _options.proxyPort === 0) { 362 | _options.proxyPort = proxyServer.address().port; 363 | } 364 | errMessage += ` Please open http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser and login to Amazon. The cookie will be output here after successfull login.`; 365 | callback && callback(new Error(errMessage), null); 366 | } 367 | ); 368 | return; 369 | } 370 | } 371 | callback && callback(new Error(errMessage), null); 372 | return; 373 | } 374 | 375 | return getCSRFFromCookies(Cookie, _options, callback); 376 | }); 377 | }); 378 | }); 379 | } else { 380 | amazonProxy.initAmazonProxy(_options, prepareResult, (server) => { 381 | if (!server) { 382 | callback && callback(new Error('Proxy Server could not be initialized. Check Logs.'), null); 383 | return; 384 | } 385 | proxyServer = server; 386 | if (!_options.proxyPort || _options.proxyPort === 0) { 387 | _options.proxyPort = proxyServer.address().port; 388 | } 389 | const errMessage = `Please open http://${_options.proxyOwnIp}:${_options.proxyPort}/ with your browser and login to Amazon. The cookie will be output here after successfull login.`; 390 | callback && callback(new Error(errMessage), null); 391 | }); 392 | } 393 | 394 | function prepareResult(err, data) { 395 | if (err || !data.authorization_code) { 396 | callback && callback(err, data.loginCookie); 397 | return; 398 | } 399 | handleTokenRegistration(_options, data, callback); 400 | } 401 | }; 402 | 403 | this.getDeviceAppName = () => { 404 | return (_options && _options.deviceAppName) || defaultAppName; 405 | }; 406 | 407 | const handleTokenRegistration = (_options, loginData, callback) => { 408 | _options.logger && _options.logger(`Handle token registration Start: ${JSON.stringify(loginData)}`); 409 | 410 | loginData.deviceAppName = _options.deviceAppName; 411 | 412 | let deviceSerial; 413 | if (!_options.formerRegistrationData || !_options.formerRegistrationData.deviceSerial) { 414 | const deviceSerialBuffer = Buffer.alloc(16); 415 | for (let i = 0; i < 16; i++) { 416 | deviceSerialBuffer.writeUInt8(Math.floor(Math.random() * 255), i); 417 | } 418 | deviceSerial = deviceSerialBuffer.toString('hex'); 419 | } else { 420 | _options.logger && _options.logger('Proxy Init: reuse deviceSerial from former data'); 421 | deviceSerial = _options.formerRegistrationData.deviceSerial; 422 | } 423 | loginData.deviceSerial = deviceSerial; 424 | 425 | const cookies = cookieTools.parse(loginData.loginCookie); 426 | Cookie = loginData.loginCookie; 427 | 428 | /* 429 | Register App 430 | */ 431 | 432 | const registerData = { 433 | 'requested_extensions': [ 434 | 'device_info', 435 | 'customer_info' 436 | ], 437 | 'cookies': { 438 | 'website_cookies': [], 439 | 'domain': `.${_options.baseAmazonPage}` 440 | }, 441 | 'registration_data': { 442 | 'domain': 'Device', 443 | 'app_version': apiCallVersion, 444 | 'device_type': 'A2IVLV5VM2W81', 445 | 'device_name': '%FIRST_NAME%\u0027s%DUPE_STRATEGY_1ST%' + _options.deviceAppName, 446 | 'os_version': '18.3.1', 447 | 'device_serial': deviceSerial, 448 | 'device_model': 'iPhone', 449 | 'app_name': _options.deviceAppName, 450 | 'software_version': '1' 451 | }, 452 | 'auth_data': { 453 | // Filled below 454 | }, 455 | 'user_context_map': { 456 | 'frc': cookies.frc 457 | }, 458 | 'requested_token_type': [ 459 | 'bearer', 460 | 'mac_dms', 461 | 'website_cookies' 462 | ] 463 | }; 464 | if (loginData.accessToken) { 465 | registerData.auth_data = { 466 | 'access_token': loginData.accessToken 467 | }; 468 | } else if (loginData.authorization_code && loginData.verifier) { 469 | registerData.auth_data = { 470 | 'client_id' : loginData.deviceId, 471 | 'authorization_code' : loginData.authorization_code, 472 | 'code_verifier' : loginData.verifier, 473 | 'code_algorithm' : 'SHA-256', 474 | 'client_domain' : 'DeviceLegacy' 475 | }; 476 | } 477 | for (const key of Object.keys(cookies)) { 478 | registerData.cookies.website_cookies.push({ 479 | 'Value': cookies[key], 480 | 'Name': key 481 | }); 482 | } 483 | 484 | const options = { 485 | host: `api.${_options.baseAmazonPage}`, 486 | path: '/auth/register', 487 | method: 'POST', 488 | headers: { 489 | 'User-Agent': apiCallUserAgent, 490 | 'Accept-Language': _options.acceptLanguage, 491 | 'Accept-Charset': 'utf-8', 492 | 'Connection': 'keep-alive', 493 | 'Content-Type': 'application/json', 494 | 'Cookie': loginData.loginCookie, 495 | 'Accept': 'application/json', 496 | 'x-amzn-identity-auth-domain': `api.${_options.baseAmazonPage}` 497 | }, 498 | body: JSON.stringify(registerData) 499 | }; 500 | _options.logger && _options.logger('Alexa-Cookie: Register App'); 501 | _options.logger && _options.logger(JSON.stringify(options)); 502 | request(options, (error, response, body) => { 503 | if (error) { 504 | callback && callback(error, null); 505 | return; 506 | } 507 | try { 508 | if (typeof body !== 'object') body = JSON.parse(body); 509 | } catch (err) { 510 | _options.logger && _options.logger(`Register App Response: ${JSON.stringify(body)}`); 511 | callback && callback(err, null); 512 | return; 513 | } 514 | _options.logger && _options.logger(`Register App Response: ${JSON.stringify(body)}`); 515 | 516 | if (!body.response || !body.response.success || !body.response.success.tokens || !body.response.success.tokens.bearer) { 517 | callback && callback(new Error('No tokens in Register response'), null); 518 | return; 519 | } 520 | Cookie = addCookies(Cookie, response.headers); 521 | loginData.refreshToken = body.response.success.tokens.bearer.refresh_token; 522 | const accessToken = body.response.success.tokens.bearer.access_token; 523 | loginData.tokenDate = Date.now(); 524 | loginData.macDms = body.response.success.tokens.mac_dms; 525 | 526 | if (body.response.success.tokens.website_cookies && Array.isArray(body.response.success.tokens.website_cookies)) { 527 | const newCookies = []; 528 | body.response.success.tokens.website_cookies.forEach(cookie => { 529 | newCookies.push(`${cookie.Name}=${cookie.Value};`); 530 | }); 531 | Cookie = addCookies(Cookie, {'set-cookie': newCookies}); 532 | } 533 | 534 | registerTokenCapabilities(accessToken, () => { 535 | /* 536 | Get Amazon Marketplace Country 537 | */ 538 | 539 | const options = { 540 | host: `alexa.${_options.baseAmazonPage}`, 541 | path: `/api/users/me?platform=ios&version=${apiCallVersion}`, 542 | method: 'GET', 543 | headers: { 544 | 'User-Agent': apiCallUserAgent, 545 | 'Accept-Language': _options.acceptLanguage, 546 | 'Accept-Charset': 'utf-8', 547 | 'Connection': 'keep-alive', 548 | 'Accept': 'application/json', 549 | 'Cookie': Cookie 550 | } 551 | }; 552 | _options.logger && _options.logger('Alexa-Cookie: Get User data'); 553 | _options.logger && _options.logger(JSON.stringify(options)); 554 | request(options, (error, response, body) => { 555 | if (!error) { 556 | try { 557 | if (typeof body !== 'object') body = JSON.parse(body); 558 | } catch (err) { 559 | _options.logger && _options.logger(`Get User data Response: ${JSON.stringify(body)}`); 560 | callback && callback(err, null); 561 | return; 562 | } 563 | _options.logger && _options.logger(`Get User data Response: ${JSON.stringify(body)}`); 564 | 565 | Cookie = addCookies(Cookie, response.headers); 566 | 567 | if (body.marketPlaceDomainName) { 568 | const pos = body.marketPlaceDomainName.indexOf('.'); 569 | if (pos !== -1) _options.amazonPage = body.marketPlaceDomainName.substr(pos + 1); 570 | } 571 | loginData.amazonPage = _options.amazonPage; 572 | } else if (error && (!_options || !_options.amazonPage)) { 573 | callback && callback(error, null); 574 | return; 575 | } else if (error && (!_options.formerRegistrationData || !_options.formerRegistrationData.amazonPage) && _options.amazonPage) { 576 | _options.logger && _options.logger(`Continue with externally set amazonPage: ${_options.amazonPage}`); 577 | } else if (error) { 578 | _options.logger && _options.logger('Ignore error while getting user data and amazonPage because previously set amazonPage is available'); 579 | } 580 | 581 | loginData.loginCookie = Cookie; 582 | 583 | getLocalCookies(loginData.amazonPage, loginData.refreshToken, (err, localCookie) => { 584 | if (err) { 585 | callback && callback(err, null); 586 | } 587 | 588 | loginData.localCookie = localCookie; 589 | getCSRFFromCookies(loginData.localCookie, _options, (err, resData) => { 590 | if (err) { 591 | callback && callback(new Error(`Error getting csrf for ${loginData.amazonPage}`), null); 592 | return; 593 | } 594 | loginData.localCookie = resData.cookie; 595 | loginData.csrf = resData.csrf; 596 | delete loginData.accessToken; 597 | delete loginData.authorization_code; 598 | delete loginData.verifier; 599 | loginData.dataVersion = 2; 600 | _options.logger && _options.logger(`Final Registration Result: ${JSON.stringify(loginData)}`); 601 | callback && callback(null, loginData); 602 | }); 603 | }); 604 | }); 605 | }); 606 | }); 607 | }; 608 | 609 | const registerTokenCapabilities = (accessToken, callback) => { 610 | /* 611 | Register Capabilities - mainly needed for HTTP/2 push infos 612 | */ 613 | const options = { 614 | host: `api.amazonalexa.com`, // How Domains needs to be for other regions? au/jp? 615 | path: `/v1/devices/@self/capabilities`, 616 | method: 'PUT', 617 | headers: { 618 | 'User-Agent': apiCallUserAgent, 619 | 'Accept-Language': _options.acceptLanguage, 620 | 'Accept-Charset': 'utf-8', 621 | 'Connection': 'keep-alive', 622 | 'Content-type': 'application/json; charset=UTF-8', 623 | 'authorization': `Bearer ${accessToken}`, 624 | }, 625 | body: '{"legacyFlags":{"SUPPORTS_COMMS":true,"SUPPORTS_ARBITRATION":true,"SCREEN_WIDTH":1170,"SUPPORTS_SCRUBBING":true,"SPEECH_SYNTH_SUPPORTS_TTS_URLS":false,"SUPPORTS_HOME_AUTOMATION":true,"SUPPORTS_DROPIN_OUTBOUND":true,"FRIENDLY_NAME_TEMPLATE":"VOX","SUPPORTS_SIP_OUTBOUND_CALLING":true,"VOICE_PROFILE_SWITCHING_DISABLED":true,"SUPPORTS_LYRICS_IN_CARD":false,"SUPPORTS_DATAMART_NAMESPACE":"Vox","SUPPORTS_VIDEO_CALLING":true,"SUPPORTS_PFM_CHANGED":true,"SUPPORTS_TARGET_PLATFORM":"TABLET","SUPPORTS_SECURE_LOCKSCREEN":false,"AUDIO_PLAYER_SUPPORTS_TTS_URLS":false,"SUPPORTS_KEYS_IN_HEADER":false,"SUPPORTS_MIXING_BEHAVIOR_FOR_AUDIO_PLAYER":false,"AXON_SUPPORT":true,"SUPPORTS_TTS_SPEECHMARKS":true},"envelopeVersion":"20160207","capabilities":[{"version":"0.1","interface":"CardRenderer","type":"AlexaInterface"},{"interface":"Navigation","type":"AlexaInterface","version":"1.1"},{"type":"AlexaInterface","version":"2.0","interface":"Alexa.Comms.PhoneCallController"},{"type":"AlexaInterface","version":"1.1","interface":"ExternalMediaPlayer"},{"type":"AlexaInterface","interface":"Alerts","configurations":{"maximumAlerts":{"timers":2,"overall":99,"alarms":2}},"version":"1.3"},{"version":"1.0","interface":"Alexa.Display.Window","type":"AlexaInterface","configurations":{"templates":[{"type":"STANDARD","id":"app_window_template","configuration":{"sizes":[{"id":"fullscreen","type":"DISCRETE","value":{"value":{"height":1440,"width":3200},"unit":"PIXEL"}}],"interactionModes":["mobile_mode","auto_mode"]}}]}},{"type":"AlexaInterface","interface":"AccessoryKit","version":"0.1"},{"type":"AlexaInterface","interface":"Alexa.AudioSignal.ActiveNoiseControl","version":"1.0","configurations":{"ambientSoundProcessingModes":[{"name":"ACTIVE_NOISE_CONTROL"},{"name":"PASSTHROUGH"}]}},{"interface":"PlaybackController","type":"AlexaInterface","version":"1.0"},{"version":"1.0","interface":"Speaker","type":"AlexaInterface"},{"version":"1.0","interface":"SpeechSynthesizer","type":"AlexaInterface"},{"version":"1.0","interface":"AudioActivityTracker","type":"AlexaInterface"},{"type":"AlexaInterface","interface":"Alexa.Camera.LiveViewController","version":"1.0"},{"type":"AlexaInterface","version":"1.0","interface":"Alexa.Input.Text"},{"type":"AlexaInterface","interface":"Alexa.PlaybackStateReporter","version":"1.0"},{"version":"1.1","interface":"Geolocation","type":"AlexaInterface"},{"interface":"Alexa.Health.Fitness","version":"1.0","type":"AlexaInterface"},{"interface":"Settings","type":"AlexaInterface","version":"1.0"},{"configurations":{"interactionModes":[{"dialog":"SUPPORTED","interactionDistance":{"value":18,"unit":"INCHES"},"video":"SUPPORTED","keyboard":"SUPPORTED","id":"mobile_mode","uiMode":"MOBILE","touch":"SUPPORTED"},{"video":"UNSUPPORTED","dialog":"SUPPORTED","interactionDistance":{"value":36,"unit":"INCHES"},"uiMode":"AUTO","touch":"SUPPORTED","id":"auto_mode","keyboard":"UNSUPPORTED"}]},"type":"AlexaInterface","interface":"Alexa.InteractionMode","version":"1.0"},{"type":"AlexaInterface","configurations":{"catalogs":[{"type":"IOS_APP_STORE","identifierTypes":["URI_HTTP_SCHEME","URI_CUSTOM_SCHEME"]}]},"version":"0.2","interface":"Alexa.Launcher"},{"interface":"System","version":"1.0","type":"AlexaInterface"},{"interface":"Alexa.IOComponents","type":"AlexaInterface","version":"1.4"},{"type":"AlexaInterface","interface":"Alexa.FavoritesController","version":"1.0"},{"version":"1.0","type":"AlexaInterface","interface":"Alexa.Mobile.Push"},{"type":"AlexaInterface","interface":"InteractionModel","version":"1.1"},{"interface":"Alexa.PlaylistController","type":"AlexaInterface","version":"1.0"},{"interface":"SpeechRecognizer","type":"AlexaInterface","version":"2.1"},{"interface":"AudioPlayer","type":"AlexaInterface","version":"1.3"},{"type":"AlexaInterface","version":"3.1","interface":"Alexa.RTCSessionController"},{"interface":"VisualActivityTracker","version":"1.1","type":"AlexaInterface"},{"interface":"Alexa.PlaybackController","version":"1.0","type":"AlexaInterface"},{"type":"AlexaInterface","interface":"Alexa.SeekController","version":"1.0"},{"interface":"Alexa.Comms.MessagingController","type":"AlexaInterface","version":"1.0"}]}' 626 | 627 | // New 628 | // {"envelopeVersion":"20160207","legacyFlags":{"SUPPORTS_TARGET_PLATFORM":"TABLET","SUPPORTS_SECURE_LOCKSCREEN":false,"SUPPORTS_DATAMART_NAMESPACE":"Vox","AXON_SUPPORT":true,"SUPPORTS_DROPIN_OUTBOUND":true,"SUPPORTS_LYRICS_IN_CARD":false,"VOICE_PROFILE_SWITCHING_DISABLED":true,"SUPPORTS_ARBITRATION":true,"SUPPORTS_HOME_AUTOMATION":true,"SUPPORTS_KEYS_IN_HEADER":false,"SUPPORTS_TTS_SPEECHMARKS":true,"AUDIO_PLAYER_SUPPORTS_TTS_URLS":false,"SUPPORTS_SIP_OUTBOUND_CALLING":true,"SUPPORTS_MIXING_BEHAVIOR_FOR_AUDIO_PLAYER":false,"SUPPORTS_COMMS":true,"SCREEN_WIDTH":1170,"SUPPORTS_VIDEO_CALLING":true,"FRIENDLY_NAME_TEMPLATE":"VOX","SUPPORTS_PFM_CHANGED":true,"SPEECH_SYNTH_SUPPORTS_TTS_URLS":false,"SUPPORTS_SCRUBBING":true},"capabilities":[{"type":"AlexaInterface","interface":"AudioPlayer","version":"1.3"},{"version":"1.0","type":"AlexaInterface","interface":"Settings"},{"interface":"System","type":"AlexaInterface","version":"1.0"},{"type":"AlexaInterface","interface":"AudioActivityTracker","version":"1.0"},{"interface":"SpeechRecognizer","version":"2.3","type":"AlexaInterface"},{"type":"AlexaInterface","interface":"Speaker","version":"1.0"},{"type":"AlexaInterface","version":"1.0","interface":"SpeechSynthesizer"},{"type":"AlexaInterface","version":"0.1","interface":"CardRenderer"},{"interface":"PlaybackController","type":"AlexaInterface","version":"1.0"},{"version":"1.1","type":"AlexaInterface","interface":"Navigation"},{"version":"1.1","type":"AlexaInterface","interface":"InteractionModel"},{"type":"AlexaInterface","version":"1.1","interface":"Geolocation"}]} 629 | }; 630 | _options.logger && _options.logger('Alexa-Cookie: Register capabilities'); 631 | _options.logger && _options.logger(JSON.stringify(options)); 632 | request(options, (error, response, body) => { 633 | if (error || (response.statusCode !== 204 && response.statusCode !== 200)) { 634 | _options.logger && _options.logger('Alexa-Cookie: Could not set capabilities, Push connection might not work!'); 635 | _options.logger && _options.logger(`Alexa-Cookie: ${JSON.stringify(error)}: ${JSON.stringify(body)}`); 636 | } 637 | callback && callback(); 638 | }); 639 | }; 640 | 641 | const getLocalCookies = (amazonPage, refreshToken, callback) => { 642 | Cookie = ''; // Reset because we are switching domains 643 | /* 644 | Token Exchange to Amazon Country Page 645 | */ 646 | 647 | const exchangeParams = { 648 | 'di.os.name': 'iOS', 649 | 'app_version': apiCallVersion, 650 | 'domain': `.${amazonPage}`, 651 | 'source_token': refreshToken, 652 | 'requested_token_type': 'auth_cookies', 653 | 'source_token_type': 'refresh_token', 654 | 'di.hw.version': 'iPhone', 655 | 'di.sdk.version': '6.12.4', 656 | 'app_name': _options.deviceAppName || defaultAppName, 657 | 'di.os.version': '16.6' 658 | }; 659 | const options = { 660 | host: `www.${amazonPage}`, 661 | path: '/ap/exchangetoken/cookies', 662 | method: 'POST', 663 | headers: { 664 | 'User-Agent': apiCallUserAgent, 665 | 'Accept-Language': _options.acceptLanguage, 666 | 'Accept-Charset': 'utf-8', 667 | 'Connection': 'keep-alive', 668 | 'Content-Type': 'application/x-www-form-urlencoded', 669 | 'Accept': '*/*', 670 | 'Cookie': Cookie, 671 | 'x-amzn-identity-auth-domain': `api.${amazonPage}` 672 | }, 673 | body: querystring.stringify(exchangeParams, null, null, { 674 | encodeURIComponent: encodeURIComponent 675 | }) 676 | }; 677 | _options.logger && _options.logger(`Alexa-Cookie: Exchange tokens for ${amazonPage}`); 678 | _options.logger && _options.logger(JSON.stringify(options)); 679 | 680 | request(options, (error, response, body) => { 681 | if (error) { 682 | callback && callback(error, null); 683 | return; 684 | } 685 | try { 686 | if (typeof body !== 'object') body = JSON.parse(body); 687 | } catch (err) { 688 | _options.logger && _options.logger(`Exchange Token Response: ${JSON.stringify(body)}`); 689 | callback && callback(err, null); 690 | return; 691 | } 692 | _options.logger && _options.logger(`Exchange Token Response: ${JSON.stringify(body)}`); 693 | 694 | if (!body.response || !body.response.tokens || !body.response.tokens.cookies) { 695 | callback && callback(new Error('No cookies in Exchange response'), null); 696 | return; 697 | } 698 | if (!body.response.tokens.cookies[`.${amazonPage}`]) { 699 | callback && callback(new Error(`No cookies for ${amazonPage} in Exchange response`), null); 700 | return; 701 | } 702 | 703 | Cookie = addCookies(Cookie, response.headers); 704 | const cookies = cookieTools.parse(Cookie); 705 | body.response.tokens.cookies[`.${amazonPage}`].forEach((cookie) => { 706 | if (cookies[cookie.Name] && cookies[cookie.Name] !== cookie.Value) { 707 | _options.logger && _options.logger(`Alexa-Cookie: Update Cookie ${cookie.Name} = ${cookie.Value}`); 708 | } else if (!cookies[cookie.Name]) { 709 | _options.logger && _options.logger(`Alexa-Cookie: Add Cookie ${cookie.Name} = ${cookie.Value}`); 710 | } 711 | cookies[cookie.Name] = cookie.Value; 712 | 713 | }); 714 | let localCookie = ''; 715 | for (const name of Object.keys(cookies)) { 716 | localCookie += `${name}=${cookies[name]}; `; 717 | } 718 | localCookie = localCookie.replace(/[; ]*$/, ''); 719 | 720 | callback && callback(null, localCookie); 721 | }); 722 | }; 723 | 724 | this.refreshAlexaCookie = (__options, callback) => { 725 | if (!__options || !__options.formerRegistrationData || !__options.formerRegistrationData.loginCookie || !__options.formerRegistrationData.refreshToken) { 726 | callback && callback(new Error('No former registration data provided for Cookie Refresh'), null); 727 | return; 728 | } 729 | 730 | if (typeof __options === 'function') { 731 | callback = __options; 732 | __options = {}; 733 | } 734 | 735 | _options = __options; 736 | 737 | __options.proxyOnly = true; 738 | 739 | initConfig(); 740 | 741 | const refreshData = { 742 | 'app_name': _options.deviceAppName || defaultAppName, 743 | 'app_version': apiCallVersion, 744 | 'di.sdk.version': '6.12.4', 745 | 'source_token': _options.formerRegistrationData.refreshToken, 746 | 'package_name': 'com.amazon.echo', 747 | 'di.hw.version': 'iPhone', 748 | 'platform': 'iOS', 749 | 'requested_token_type': 'access_token', 750 | 'source_token_type': 'refresh_token', 751 | 'di.os.name': 'iOS', 752 | 'di.os.version': '16.6', 753 | 'current_version': '6.12.4' 754 | }; 755 | 756 | const options = { 757 | host: `api.${_options.baseAmazonPage}`, 758 | path: '/auth/token', 759 | method: 'POST', 760 | headers: { 761 | 'User-Agent': apiCallUserAgent, 762 | 'Accept-Language': _options.acceptLanguage, 763 | 'Accept-Charset': 'utf-8', 764 | 'Connection': 'keep-alive', 765 | 'Content-Type': 'application/x-www-form-urlencoded', 766 | 'Cookie': _options.formerRegistrationData.loginCookie, 767 | 'Accept': 'application/json', 768 | 'x-amzn-identity-auth-domain': `api.${_options.baseAmazonPage}` 769 | }, 770 | body: querystring.stringify(refreshData) 771 | }; 772 | Cookie = _options.formerRegistrationData.loginCookie; 773 | _options.logger && _options.logger('Alexa-Cookie: Refresh Token'); 774 | _options.logger && _options.logger(JSON.stringify(options)); 775 | request(options, (error, response, body) => { 776 | if (error) { 777 | callback && callback(error, null); 778 | return; 779 | } 780 | try { 781 | if (typeof body !== 'object') body = JSON.parse(body); 782 | } catch (err) { 783 | _options.logger && _options.logger(`Refresh Token Response: ${JSON.stringify(body)}`); 784 | callback && callback(err, null); 785 | return; 786 | } 787 | _options.logger && _options.logger(`Refresh Token Response: ${JSON.stringify(body)}`); 788 | 789 | _options.formerRegistrationData.loginCookie = addCookies(_options.formerRegistrationData.loginCookie, response.headers); 790 | 791 | if (!body.access_token) { 792 | callback && callback(new Error('No new access token in Refresh Token response'), null); 793 | return; 794 | } 795 | _options.formerRegistrationData.loginCookie = addCookies(Cookie, response.headers); 796 | _options.formerRegistrationData.accessToken = body.access_token; 797 | 798 | getLocalCookies(_options.baseAmazonPage, _options.formerRegistrationData.refreshToken, (err, comCookie) => { 799 | if (err) { 800 | callback && callback(err, null); 801 | } 802 | 803 | // Restore frc and map-md 804 | const initCookies = cookieTools.parse(_options.formerRegistrationData.loginCookie); 805 | let newCookie = `frc=${initCookies.frc}; `; 806 | newCookie += `map-md=${initCookies['map-md']}; `; 807 | newCookie += comCookie; 808 | 809 | _options.formerRegistrationData.loginCookie = newCookie; 810 | handleTokenRegistration(_options, _options.formerRegistrationData, callback); 811 | }); 812 | }); 813 | }; 814 | 815 | this.stopProxyServer = (callback) => { 816 | if (proxyServer) { 817 | proxyServer.close(() => { 818 | callback && callback(); 819 | }); 820 | } 821 | proxyServer = null; 822 | }; 823 | } 824 | 825 | module.exports = AlexaCookie(); 826 | --------------------------------------------------------------------------------