├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .make-release-support ├── .prettierignore ├── .prettierrc ├── .release ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── _config.yml ├── babel.config.js ├── docs ├── OAuth2Node.html ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ └── OpenSans-Regular-webfont.woff ├── oauth2.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js └── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── examples ├── Sandbox PayPal - Client Credentials.json └── node-red-contrib-oauth2.json ├── jsdoc.json ├── package-lock.json ├── package.json ├── src ├── icons │ └── oauth2.svg ├── libs │ └── logger.js ├── locales │ └── en-US │ │ ├── oauth2.html │ │ └── oauth2.json ├── oauth2.html └── oauth2.js ├── test ├── logger_spec.js ├── node-loading_spec.js ├── oauth2 │ ├── oauth2-edge-cases_spec.js │ ├── oauth2-error-handling_spec.js │ ├── oauth2-flows_spec.js │ ├── oauth2-generate-options_spec.js │ ├── oauth2-grant-types_spec.js │ ├── oauth2-loading_spec.js │ └── oauth2-post-request_spec.js ├── setup-teardown._spec.js └── utils │ ├── certificate.pem │ ├── private-key.pem │ └── proxy.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es6: true 4 | node: true 5 | mocha: true 6 | extends: 7 | - "eslint:recommended" 8 | - "plugin:prettier/recommended" 9 | parser: "@babel/eslint-parser" 10 | parserOptions: 11 | requireConfigFile: false 12 | babelOptions: 13 | configFile: "./babel.config.js" 14 | rules: 15 | indent: 16 | - error 17 | - 3 18 | linebreak-style: 19 | - error 20 | - unix 21 | quotes: 22 | - error 23 | - single 24 | semi: 25 | - error 26 | - always 27 | no-console: "off" 28 | plugins: 29 | - mocha 30 | 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | ### Which node are you reporting an issue on? 22 | 23 | ### What are the steps to reproduce? 24 | 25 | ### What happens? 26 | 27 | ### What do you expect to happen? 28 | 29 | ### Please tell us about your environment: 30 | 31 | - [ ] Node-RED version: 32 | - [ ] node.js version: 33 | - [ ] npm version: 34 | - [ ] Platform/OS: 35 | - [ ] Browser: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | 16 | 23 | 24 | ## Proposed changes 25 | 26 | 27 | 28 | ## Checklist 29 | 30 | 31 | - [ ] I have read the [contribution guidelines](https://github.com/node-red/node-red-nodes/blob/master/CONTRIBUTING.md) 32 | - [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team. 33 | - [ ] I have run `grunt` to verify the unit tests pass 34 | - [ ] I have added suitable unit tests to cover the new/changed functionality -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | /node_modules 4 | /lib 5 | index.html 6 | coverage 7 | .nyc_output 8 | proxy.pid 9 | proxy.log 10 | proxy.* -------------------------------------------------------------------------------- /.make-release-support: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if there are changes in the current Git repository 4 | function hasChanges() { 5 | test -n "$(git status -s .)" 6 | } 7 | 8 | # Get the release version from the .release file 9 | function getRelease() { 10 | if [[ -f .release ]]; then 11 | awk -F= '/release/{print $2}' .release 12 | fi 13 | } 14 | 15 | # Get the base tag from the .release file 16 | function getBaseTag() { 17 | # Extract the tag value and remove the release version from it 18 | sed -n -e "s/tag=\\(.*\\)$(getRelease)\$/\\1/p" .release 19 | } 20 | 21 | # Get the complete tag for the current release 22 | function getTag() { 23 | if [[ -z "$1" ]] ; then 24 | # If no version number is specified, return the tag from the .release file 25 | awk -F= '/tag/{print $2}' .release 26 | else 27 | # Otherwise, append the specified version number to the base tag 28 | echo "$(getBaseTag)$1" 29 | fi 30 | } 31 | 32 | # Set the release version in the .release file and update the package.json file 33 | function setRelease() { 34 | if [[ -n "$1" ]] ; then 35 | # Replace the tag and release version in the .release file 36 | sed -i.x -e "s/tag=.*/tag=$(getTag "$1")/" .release 37 | sed -i.x -e "s/release=.*/release=$1/g" .release 38 | # Update the version number in the package.json file 39 | sed -i.x -e '0,/"version": "[^"]*"/s//"version": "'$1'"/' package.json 40 | # Remove backup files created by sed 41 | rm -f .release.x 42 | rm -f package.json.x 43 | else 44 | # Print an error message if no version number is specified 45 | echo "ERROR: missing release version parameter " >&2 46 | return 1 47 | fi 48 | } 49 | 50 | # Check if a tag already exists for the current release 51 | function tagExists() { 52 | # Use the specified tag or the current tag if none is specified 53 | tag=${1:-$(getTag)} 54 | # Check if the tag exists in Git 55 | test -n "$tag" && test -n "$(git tag | grep "^$tag\$")" 56 | } 57 | 58 | # Check if the current Git repository differs from the current release 59 | function differsFromRelease() { 60 | tag=$(getTag) 61 | # Check if the tag does not exist or if there are differences between the current state and the tag 62 | ! tagExists "$tag" || test -n "$(git diff --shortstat -r "$tag" .)" 63 | } 64 | 65 | # Get the version number for the current release 66 | function getVersion() { 67 | result=$(getRelease) 68 | 69 | if differsFromRelease; then 70 | # Append the short hash of the latest commit if there are differences between the current state and the tag 71 | result="$result-$(git rev-parse --short HEAD)" 72 | fi 73 | 74 | if hasChanges ; then 75 | # Append "-dirty" if there are uncommitted changes in the Git repository 76 | result="$result-dirty" 77 | fi 78 | echo "$result" 79 | } 80 | 81 | function nextMicroLevel() { 82 | version=${1:-$(getRelease)} 83 | # Increment the micro-level version number by 1 84 | major_and_minor=$(echo "$version" | cut -d. -f1,2) 85 | micro=$(echo "$version" | cut -d. -f3) 86 | version=$(printf "%s.%d" "$major_and_minor" $((micro + 1))) 87 | echo "$version" 88 | } 89 | # Get the next minor-level version number for the current release 90 | function nextMinorLevel() { 91 | version=${1:-$(getRelease)} 92 | major=$(echo "$version" | cut -d. -f1); 93 | minor=$(echo "$version" | cut -d. -f2); 94 | version=$(printf "%d.%d.0" "$major" $((minor + 1))) ; 95 | echo "$version" 96 | } 97 | 98 | # Get the next major-level version number for the current release 99 | function nextMajorLevel() { 100 | version=${1:-$(getRelease)} 101 | major=$(echo "$version" | cut -d. -f1); 102 | version=$(printf "%d.0.0" $((major + 1))) 103 | echo "$version" 104 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.md 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 3, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "semi": true, 7 | "printWidth": 250, 8 | "pluginSearchDirs": [ 9 | "." 10 | ], 11 | "overrides": [] 12 | } 13 | -------------------------------------------------------------------------------- /.release: -------------------------------------------------------------------------------- 1 | release=6.2.1 2 | tag=6.2.1 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marcos Caputo (caputomarcos) 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | PROJECT_NAME=$(shell basename $(PWD)) 4 | REGISTRY_HOST= 5 | REGISTRY_GROUP= 6 | 7 | RELEASE_SUPPORT := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))/.make-release-support 8 | IMAGE=$(shell tr '[:upper:]' '[:lower:]' <<< $(PROJECT_NAME)) 9 | 10 | VERSION=$(shell . $(RELEASE_SUPPORT) ; getVersion) 11 | TAG=$(shell . $(RELEASE_SUPPORT); getTag) 12 | 13 | .PHONY: help version release lint test coverage start stop status log 14 | 15 | help: 16 | @echo 'Usage: make ' 17 | @echo '' 18 | @echo ' Targets:' 19 | @echo '' 20 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-30s\033[0m %s\n", $$1, $$2}' 21 | @echo '' 22 | 23 | version: .release ## Display Release Version. 24 | @. $(RELEASE_SUPPORT); getVersion 25 | 26 | .release: 27 | @echo "release=0.0.0" > .release 28 | @echo "tag=$(PROJECT_NAME)-0.0.0" >> .release 29 | @echo INFO: .release created 30 | @cat .release 31 | 32 | check-release: .release 33 | @. $(RELEASE_SUPPORT) ; tagExists $(TAG) || (echo "ERROR: version not yet tagged in git. make [major,minor,micro]-release." >&2 && exit 1) ; 34 | @. $(RELEASE_SUPPORT) ; ! differsFromRelease $(TAG) || (echo "ERROR: current directory differs from tagged $(TAG). make [major,minor,micro]-release." ; exit 1) 35 | 36 | check-status: 37 | @. $(RELEASE_SUPPORT) ; ! hasChanges || (echo "ERROR: there are still outstanding changes" >&2 && exit 1) ; 38 | 39 | tag: TAG=$(shell . $(RELEASE_SUPPORT); getTag $(VERSION)) 40 | tag: check-status 41 | @echo "Current version: $(VERSION)" # Debug statement 42 | @echo "Current tag: $(TAG)" # Debug statement 43 | @. $(RELEASE_SUPPORT) ; ! tagExists $(TAG) || (echo "ERROR: tag $(TAG) for version $(VERSION) already tagged in git" >&2 && exit 1) ; 44 | @. $(RELEASE_SUPPORT) ; setRelease $(VERSION) 45 | git add .release package.json 46 | git commit -m "bumped to version $(VERSION)" 47 | git tag $(TAG) 48 | @[ -n "$(shell git remote -v)" ] && git push --tags 49 | 50 | tag-major-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextMajorLevel) 51 | tag-major-release: .release tag 52 | major-release: tag-major-release ## Major Release 53 | @echo $(VERSION) 54 | 55 | tag-minor-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextMinorLevel) 56 | tag-minor-release: .release tag 57 | minor-release: tag-minor-release ## Minor Release 58 | @echo $(VERSION) 59 | 60 | tag-micro-release: VERSION := $(shell . $(RELEASE_SUPPORT); nextMicroLevel) 61 | tag-micro-release: .release tag 62 | micro-release: tag-micro-release ## Micro Release 63 | @echo $(VERSION) 64 | 65 | lint: ## Lint the project 66 | npm run lint 67 | 68 | test: ## Run tests 69 | npm test 70 | 71 | coverage: ## Generate test coverage 72 | npm run coverage 73 | 74 | PID_FILE=proxy.pid 75 | LOG_FILE=proxy.log 76 | 77 | start: ## Start the proxy server 78 | @echo "Starting proxy server..." 79 | @nohup node test/utils/proxy.js > $(LOG_FILE) 2>&1 & echo $$! > $(PID_FILE) 80 | @echo "Proxy server started with PID $$(cat $(PID_FILE))" 81 | 82 | stop: ## Stop the proxy server 83 | @if [ -f $(PID_FILE) ]; then \ 84 | echo "Stopping proxy server..."; \ 85 | PID=$$(cat $(PID_FILE)); \ 86 | if kill $$PID 2>/dev/null; then \ 87 | rm -f $(PID_FILE); \ 88 | echo "Proxy server stopped"; \ 89 | else \ 90 | echo "Proxy server not running, removing stale PID file"; \ 91 | rm -f $(PID_FILE); \ 92 | fi \ 93 | else \ 94 | echo "Proxy server is not running"; \ 95 | fi 96 | 97 | status: ## Check the status of the proxy server 98 | @if [ -f $(PID_FILE) ]; then \ 99 | PID=$$(cat $(PID_FILE)); \ 100 | if ps -p $$PID > /dev/null; then \ 101 | echo "Proxy server is running with PID $$PID"; \ 102 | else \ 103 | echo "Proxy server is not running, but PID file exists"; \ 104 | fi \ 105 | else \ 106 | echo "Proxy server is not running"; \ 107 | fi 108 | 109 | log: ## Tail the proxy server logs 110 | @tail -f $(LOG_FILE) -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current' 8 | } 9 | } 10 | ] 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caputomarcos/node-red-contrib-oauth2/8548c5e77dac766efff406bb32eb4100e23c7487/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/oauth2.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: oauth2.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: oauth2.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
module.exports = function (RED) {
 30 |    'use strict';
 31 | 
 32 |    const axios = require('axios');
 33 |    const http = require('http');
 34 |    const https = require('https');
 35 |    const { URLSearchParams } = require('url'); // Use URLSearchParams for form data
 36 |    const Logger = require('node-red-contrib-oauth2/src/libs/logger');
 37 | 
 38 |    /**
 39 |     * Class representing an OAuth2 Node.
 40 |     */
 41 |    class OAuth2Node {
 42 |       /**
 43 |        * Create an OAuth2Node.
 44 |        * @param {Object} config - Node configuration object.
 45 |        */
 46 |       constructor(config) {
 47 |          RED.nodes.createNode(this, config);
 48 |          this.logger = new Logger({ name: 'identifier', count: null, active: config.debug || false, label: 'debug' });
 49 |          this.logger.debug('Constructor: Initializing node with config', config);
 50 | 
 51 |          // Node configuration properties
 52 |          this.name = config.name || '';
 53 |          this.container = config.container || '';
 54 |          this.access_token_url = config.access_token_url || '';
 55 |          this.redirect_uri = config.redirect_uri || '';
 56 |          this.grant_type = config.grant_type || '';
 57 |          this.refresh_token = config.refresh_token || '';
 58 |          this.username = config.username || '';
 59 |          this.password = config.password || '';
 60 |          this.client_id = config.client_id || '';
 61 |          this.client_secret = config.client_secret || '';
 62 |          this.scope = config.scope || '';
 63 |          this.resource = config.resource || '';
 64 |          this.state = config.state || '';
 65 |          this.rejectUnauthorized = config.rejectUnauthorized || false;
 66 |          this.client_credentials_in_body = config.client_credentials_in_body || false;
 67 |          this.headers = config.headers || {};
 68 |          this.sendErrorsToCatch = config.senderr || false;
 69 | 
 70 |          // Proxy settings from environment variables or configuration
 71 |          this.prox = process.env.http_proxy || process.env.HTTP_PROXY || config.proxy;
 72 |          this.noprox = (process.env.no_proxy || process.env.NO_PROXY || '').split(',');
 73 | 
 74 |          this.logger.debug('Constructor: Finished setting up node properties');
 75 | 
 76 |          // Register the input handler
 77 |          this.on('input', this.onInput.bind(this));
 78 |          this.host = RED.settings.uiHost || 'localhost';
 79 |          this.logger.debug('Constructor: Node input handler registered');
 80 |       }
 81 | 
 82 |       /**
 83 |        * Handles input messages.
 84 |        * @param {Object} msg - Input message object.
 85 |        * @param {Function} send - Function to send messages.
 86 |        * @param {Function} done - Function to indicate processing is complete.
 87 |        */
 88 |       async onInput(msg, send, done) {
 89 |          // this.debug ? this.logger.setOn() : this.logger.setOff();
 90 |          this.logger.debug('onInput: Received message', msg);
 91 | 
 92 |          const options = this.generateOptions(msg); // Generate request options
 93 |          this.logger.debug('onInput: Generated request options', options);
 94 | 
 95 |          this.configureProxy(); // Configure proxy settings
 96 |          this.logger.debug('onInput: Configured proxy settings', this.prox);
 97 | 
 98 |          delete msg.oauth2Request; // Remove oauth2Request from msg
 99 |          this.logger.debug('onInput: Removed oauth2Request from message');
100 | 
101 |          options.form = this.cleanForm(options.form); // Clean the form data
102 |          this.logger.debug('onInput: Cleaned form data', options.form);
103 | 
104 |          try {
105 |             const response = await this.makePostRequest(options); // Make the POST request
106 |             this.logger.debug('onInput: POST request response', response);
107 |             this.handleResponse(response, msg, send); // Handle the response
108 |          } catch (error) {
109 |             this.logger.error('onInput: Error making POST request', error);
110 |             this.handleError(error, msg, send); // Handle any errors
111 |          }
112 | 
113 |          done(); // Indicate that processing is complete
114 |          this.logger.debug('onInput: Finished processing input');
115 |       }
116 | 
117 |       /**
118 |        * Generates options for the HTTP request.
119 |        * @param {Object} msg - Input message object.
120 |        * @returns {Object} - The request options.
121 |        */
122 |       generateOptions(msg) {
123 |          // Log the start of the option generation process with the input message
124 |          this.logger.debug('generateOptions: Configuring options with message', msg);
125 | 
126 |          // Initialize the form object to hold the form data
127 |          let form = {};
128 |          // Set the default URL to the access token URL configured in the node
129 |          let url = this.access_token_url;
130 |          // Initialize headers with default Content-Type and Accept headers
131 |          let headers = {
132 |             'Content-Type': 'application/x-www-form-urlencoded',
133 |             Accept: 'application/json'
134 |          };
135 | 
136 |          // Retrieve credentials from the message if available, otherwise use an empty object
137 |          const creds = msg.oauth2Request ? msg.oauth2Request.credentials || {} : {};
138 |          // Initialize the form data with grant_type, scope, resource, and state
139 |          form = {
140 |             grant_type: creds.grant_type || this.grant_type,
141 |             scope: creds.scope || this.scope,
142 |             resource: creds.resource || this.resource,
143 |             state: creds.state || this.state
144 |          };
145 | 
146 |          // Define functions for different OAuth2 flows
147 |          const flows = {
148 |             // Password flow function
149 |             password: () => {
150 |                this.logger.debug('generateOptions: Password flow detected');
151 |                form.username = creds.username || this.username;
152 |                form.password = creds.password || this.password;
153 |             },
154 |             // Client credentials flow function
155 |             client_credential: () => {
156 |                this.logger.debug('generateOptions: Client credentials flow detected');
157 |                form.client_id = creds.client_id || this.client_id;
158 |                form.client_secret = creds.client_secret || this.client_secret;
159 |             },
160 |             // Refresh token flow function
161 |             refresh_token: () => {
162 |                this.logger.debug('generateOptions: Refresh token flow detected');
163 |                form.client_id = creds.client_id || this.client_id;
164 |                form.client_secret = creds.client_secret || this.client_secret;
165 |                form.refresh_token = creds.refresh_token || this.refresh_token;
166 |             },
167 |             // Authorization code flow function
168 |             authorization_code: () => {
169 |                this.logger.debug('generateOptions: Authorization code flow detected');
170 |                const credentials = RED.nodes.getCredentials(this.id) || {};
171 |                if (credentials) {
172 |                   form.code = credentials.code;
173 |                   form.redirect_uri = this.redirect_uri;
174 |                }
175 |             },
176 |             // Implicit flow function
177 |             implicit_flow: () => {
178 |                this.logger.debug('generateOptions: Implicit flow detected');
179 |                const credentials = RED.nodes.getCredentials(this.id) || {};
180 |                if (credentials) {
181 |                   form.client_id = this.client_id;
182 |                   form.client_secret = this.client_secret;
183 |                   form.code = credentials.code;
184 |                   form.grant_type = 'authorization_code';
185 |                   form.redirect_uri = this.redirect_uri;
186 |                }
187 |             },
188 |             // Set by credentials function
189 |             set_by_credentials: () => {
190 |                this.logger.debug('generateOptions: Set by credentials flow detected');
191 |                if (msg.oauth2Request) {
192 |                   const credentials = msg.oauth2Request.credentials || {};
193 |                   form.client_id = credentials.client_id || this.client_id;
194 |                   form.client_secret = credentials.client_secret || this.client_secret;
195 |                   form.refresh_token = credentials.refresh_token || '';
196 |                }
197 |             }
198 |          };
199 | 
200 |          // Check if the grant type from the credentials is supported and call the corresponding function
201 |          if (creds.grant_type && flows[creds.grant_type]) {
202 |             flows[creds.grant_type]();
203 |          }
204 |          // Check if the default grant type of the node is supported and call the corresponding function
205 |          else if (this.grant_type && flows[this.grant_type]) {
206 |             flows[this.grant_type]();
207 |          }
208 | 
209 |          // Check if client credentials should be included in the body
210 |          if (this.client_credentials_in_body) {
211 |             this.logger.debug('generateOptions: Client credentials in body detected, using credentials');
212 |             form.client_id = creds.client_id || this.client_id;
213 |             form.client_secret = creds.client_secret || this.client_secret;
214 |          } else {
215 |             // Otherwise, add the Authorization header with client credentials encoded in base64
216 |             headers.Authorization = 'Basic ' + Buffer.from(`${creds.client_id || this.client_id}:${creds.client_secret || this.client_secret}`).toString('base64');
217 |          }
218 | 
219 |          // Set the URL to the access token URL from the message if available, otherwise use the default
220 |          url = msg.oauth2Request ? msg.oauth2Request.access_token_url || this.access_token_url : this.access_token_url;
221 | 
222 |          // Log the final generated options
223 |          this.logger.debug('generateOptions: Returning options', { method: 'POST', url, headers, form });
224 |          // Return the HTTP request options
225 |          return {
226 |             method: 'POST',
227 |             url: url,
228 |             headers: { ...headers, ...this.headers },
229 |             rejectUnauthorized: this.rejectUnauthorized,
230 |             form: form
231 |          };
232 |       }
233 | 
234 |       /**
235 |        * Configures proxy settings.
236 |        */
237 |       configureProxy() {
238 |          if (!this.prox) return;
239 | 
240 |          const proxyURL = new URL(this.prox);
241 |          this.proxy = {
242 |             protocol: proxyURL.protocol,
243 |             hostname: proxyURL.hostname,
244 |             port: proxyURL.port,
245 |             username: proxyURL.username || null,
246 |             password: proxyURL.password || null
247 |          };
248 | 
249 |          this.logger.debug('configureProxy: Proxy configured', this.proxy);
250 |       }
251 | 
252 |       /**
253 |        * Cleans form data by removing undefined or empty values.
254 |        * @param {Object} form - The form data.
255 |        * @returns {Object} - The cleaned form data.
256 |        */
257 |       cleanForm(form) {
258 |          const cleanedForm = Object.fromEntries(Object.entries(form).filter(([, value]) => value !== undefined && value !== ''));
259 |          this.logger.debug('cleanForm: Cleaned form data', cleanedForm);
260 |          return cleanedForm;
261 |       }
262 | 
263 |       /**
264 |        * Makes a POST request.
265 |        * @param {Object} options - The request options.
266 |        * @returns {Promise<Object>} - The response from the request.
267 |        */
268 |       async makePostRequest(options) {
269 |          this.logger.debug('makePostRequest: Making POST request with options', options);
270 | 
271 |          const axiosOptions = {
272 |             method: options.method,
273 |             url: options.url,
274 |             headers: options.headers,
275 |             data: new URLSearchParams(options.form).toString(),
276 |             proxy: false,
277 |             httpAgent: new http.Agent({ rejectUnauthorized: options.rejectUnauthorized }),
278 |             httpsAgent: new https.Agent({ rejectUnauthorized: options.rejectUnauthorized })
279 |          };
280 | 
281 |          if (this.proxy) {
282 |             const HttpsProxyAgent = require('https-proxy-agent');
283 |             axiosOptions.httpsAgent = new HttpsProxyAgent(this.proxy);
284 |          }
285 | 
286 |          this.logger.debug('makePostRequest: Axios request options prepared', axiosOptions);
287 | 
288 |          return axios(axiosOptions).catch((error) => {
289 |             this.logger.error('makePostRequest: Error during POST request', error);
290 |             throw error;
291 |          });
292 |       }
293 | 
294 |       /**
295 |        * Handles the response from the POST request.
296 |        * @param {Object} response - The response object.
297 |        * @param {Object} msg - Input message object.
298 |        * @param {Function} send - Function to send messages.
299 |        */
300 |       handleResponse(response, msg, send) {
301 |          this.logger.debug('handleResponse: Handling response', response);
302 | 
303 |          if (!response || !response.data) {
304 |             this.logger.warn('handleResponse: Invalid response data', response);
305 |             this.handleError({ message: 'Invalid response data' }, msg, send);
306 |             return;
307 |          }
308 | 
309 |          msg.oauth2Response = response.data || {};
310 |          msg.headers = response.headers || {}; // Include headers in the message
311 |          this.setStatus('green', `HTTP ${response.status}, ok`);
312 |          this.logger.debug('handleResponse: Response data set in message', msg);
313 |          send(msg);
314 |       }
315 | 
316 |       /**
317 |        * Handles errors from the POST request.
318 |        * @param {Object} error - The error object.
319 |        * @param {Object} msg - Input message object.
320 |        * @param {Function} send - Function to send messages.
321 |        */
322 |       handleError(error, msg, send) {
323 |          this.logger.error('handleError: Handling error', error);
324 | 
325 |          const status = error.response ? error.response.status : error.code;
326 |          const message = error.response ? error.response.statusText : error.message;
327 |          const data = error.response && error.response.data ? error.response.data : {};
328 |          const headers = error.response ? error.response.headers : {};
329 |          msg.oauth2Error = { status, message, data, headers };
330 |          this.setStatus('red', `HTTP ${status}, ${message}`);
331 |          this.logger.debug('handleError: Error data set in message', msg);
332 | 
333 |          if (this.sendErrorsToCatch) {
334 |             send([null, msg]);
335 |          } else {
336 |             this.error(msg);
337 |             send([null, msg]);
338 |          }
339 |       }
340 | 
341 |       /**
342 |        * Sets the status of the node.
343 |        * @param {string} color - The color of the status indicator.
344 |        * @param {string} text - The status text.
345 |        */
346 |       setStatus(color, text) {
347 |          this.logger.debug('setStatus: Setting status', { color, text });
348 |          this.status({ fill: color, shape: 'dot', text });
349 |          setTimeout(() => {
350 |             this.status({});
351 |          }, 250);
352 |       }
353 |    }
354 | 
355 |    /**
356 |     * Endpoint to retrieve OAuth2 credentials based on a token.
357 |     * @param {Object} req - The request object.
358 |     * @param {Object} res - The response object.
359 |     */
360 |    RED.httpAdmin.get('/oauth2/credentials/:token', (req, res) => {
361 |       try {
362 |          const credentials = RED.nodes.getCredentials(req.params.token);
363 |          if (credentials) {
364 |             res.json({ code: credentials.code, redirect_uri: credentials.redirect_uri });
365 |          } else {
366 |             res.status(404).send('oauth2.error.no-credentials');
367 |          }
368 |       } catch (error) {
369 |          res.status(500).send('oauth2.error.server-error');
370 |       }
371 |    });
372 | 
373 |    /**
374 |     * Endpoint to handle OAuth2 redirect and store the authorization code.
375 |     * @param {Object} req - The request object.
376 |     * @param {Object} res - The response object.
377 |     */
378 |    RED.httpAdmin.get('/oauth2/redirect', (req, res) => {
379 |       if (req.query.code) {
380 |          const [node_id] = req.query.state.split(':');
381 |          let credentials = RED.nodes.getCredentials(node_id);
382 | 
383 |          if (!credentials) {
384 |             credentials = {};
385 |          }
386 | 
387 |          credentials = { ...credentials, ...req.query };
388 |          RED.nodes.addCredentials(node_id, credentials);
389 | 
390 |          res.send(`
391 |                <HTML>
392 |                    <HEAD>
393 |                        <script language="javascript" type="text/javascript">
394 |                            function closeWindow() {
395 |                                window.open('', '_parent', '');
396 |                                window.close();
397 |                            }
398 |                            function delay() {
399 |                                setTimeout("closeWindow()", 1000);
400 |                            }
401 |                        </script>
402 |                    </HEAD>
403 |                    <BODY onload="javascript:delay();">
404 |                        <p>Success! This page can be closed if it doesn't do so automatically.</p>
405 |                    </BODY>
406 |                </HTML>
407 |            `);
408 |       } else {
409 |          res.status(400).send('oauth2.error.no-credentials');
410 |       }
411 |    });
412 | 
413 |    // Register the OAuth2Node node type
414 |    RED.nodes.registerType('oauth2', OAuth2Node, {
415 |       credentials: {
416 |          clientId: { type: 'text' },
417 |          clientSecret: { type: 'password' },
418 |          accessToken: { type: 'password' },
419 |          refreshToken: { type: 'password' },
420 |          expireTime: { type: 'password' },
421 |          code: { type: 'password' }
422 |       }
423 |    });
424 | };
425 | 
426 |
427 |
428 | 429 | 430 | 431 | 432 |
433 | 434 | 437 | 438 |
439 | 440 | 443 | 444 | 445 | 446 | 447 | 448 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (() => { 3 | const source = document.getElementsByClassName('prettyprint source linenums'); 4 | let i = 0; 5 | let lineNumber = 0; 6 | let lineId; 7 | let lines; 8 | let totalLines; 9 | let anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = `line${lineNumber}`; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p th:last-child { border-right: 1px solid #ddd; } 224 | 225 | .ancestors, .attribs { color: #999; } 226 | .ancestors a, .attribs a 227 | { 228 | color: #999 !important; 229 | text-decoration: none; 230 | } 231 | 232 | .clear 233 | { 234 | clear: both; 235 | } 236 | 237 | .important 238 | { 239 | font-weight: bold; 240 | color: #950B02; 241 | } 242 | 243 | .yes-def { 244 | text-indent: -1000px; 245 | } 246 | 247 | .type-signature { 248 | color: #aaa; 249 | } 250 | 251 | .name, .signature { 252 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 253 | } 254 | 255 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 256 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 257 | .details dd { margin-left: 70px; } 258 | .details ul { margin: 0; } 259 | .details ul { list-style-type: none; } 260 | .details li { margin-left: 30px; padding-top: 6px; } 261 | .details pre.prettyprint { margin: 0 } 262 | .details .object-value { padding-top: 0; } 263 | 264 | .description { 265 | margin-bottom: 1em; 266 | margin-top: 1em; 267 | } 268 | 269 | .code-caption 270 | { 271 | font-style: italic; 272 | font-size: 107%; 273 | margin: 0; 274 | } 275 | 276 | .source 277 | { 278 | border: 1px solid #ddd; 279 | width: 80%; 280 | overflow: auto; 281 | } 282 | 283 | .prettyprint.source { 284 | width: inherit; 285 | } 286 | 287 | .source code 288 | { 289 | font-size: 100%; 290 | line-height: 18px; 291 | display: block; 292 | padding: 4px 12px; 293 | margin: 0; 294 | background-color: #fff; 295 | color: #4D4E53; 296 | } 297 | 298 | .prettyprint code span.line 299 | { 300 | display: inline-block; 301 | } 302 | 303 | .prettyprint.linenums 304 | { 305 | padding-left: 70px; 306 | -webkit-user-select: none; 307 | -moz-user-select: none; 308 | -ms-user-select: none; 309 | user-select: none; 310 | } 311 | 312 | .prettyprint.linenums ol 313 | { 314 | padding-left: 0; 315 | } 316 | 317 | .prettyprint.linenums li 318 | { 319 | border-left: 3px #ddd solid; 320 | } 321 | 322 | .prettyprint.linenums li.selected, 323 | .prettyprint.linenums li.selected * 324 | { 325 | background-color: lightyellow; 326 | } 327 | 328 | .prettyprint.linenums li * 329 | { 330 | -webkit-user-select: text; 331 | -moz-user-select: text; 332 | -ms-user-select: text; 333 | user-select: text; 334 | } 335 | 336 | .params .name, .props .name, .name code { 337 | color: #4D4E53; 338 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 339 | font-size: 100%; 340 | } 341 | 342 | .params td.description > p:first-child, 343 | .props td.description > p:first-child 344 | { 345 | margin-top: 0; 346 | padding-top: 0; 347 | } 348 | 349 | .params td.description > p:last-child, 350 | .props td.description > p:last-child 351 | { 352 | margin-bottom: 0; 353 | padding-bottom: 0; 354 | } 355 | 356 | .disabled { 357 | color: #454545; 358 | } 359 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /examples/Sandbox PayPal - Client Credentials.json: -------------------------------------------------------------------------------- 1 | [{"id":"3db3b0fd97716db6","type":"tab","label":"Sandbox PayPal - Client Credentials","disabled":false,"info":"","env":[]},{"id":"77cb8766f3d0f394","type":"group","z":"3db3b0fd97716db6","style":{"stroke":"#252525","stroke-opacity":"1","fill":"#333333","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#dedede"},"nodes":["ba4c69bc6934eaf0","56941dc7fa2c9109","ea5f619acb7c6975","c6338a46953ce90f","0f0959f25b8ffac2","d501a2a68aed65ff","74cb073642513c64","b438631713b789b3","ba9065365539726b","7ae923df938ff7c2","2c022952654e4a88","7b99c317fa69b979","50f21b0248ad11fa","1e9fe7a88f8a9fd5","9f0816024a976737","23fd9bb05a9e0890","a5913a06e02ebba5","e398f17b55741fad"],"x":14,"y":19,"w":1032,"h":488},{"id":"a5913a06e02ebba5","type":"group","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","style":{"stroke":"#2e333a","stroke-opacity":"1","fill":"#383c45","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["4a659fbacbd5a9ba","f7a09a555df4434c","4eb9ce10f8328334","ba768d993577dd9a","c22f0bf46fb98541"],"x":264,"y":359,"w":742,"h":122},{"id":"23fd9bb05a9e0890","type":"junction","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","x":620,"y":220,"wires":[["ba4c69bc6934eaf0"]]},{"id":"c22f0bf46fb98541","type":"junction","z":"3db3b0fd97716db6","g":"a5913a06e02ebba5","x":760,"y":440,"wires":[["ba768d993577dd9a","4eb9ce10f8328334"]]},{"id":"e398f17b55741fad","type":"junction","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","x":860,"y":160,"wires":[["ea5f619acb7c6975"]]},{"id":"ba4c69bc6934eaf0","type":"http request","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":730,"y":220,"wires":[["e398f17b55741fad"]]},{"id":"56941dc7fa2c9109","type":"function","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"POST credit-cards","func":"let token = global.get('token');\nmsg = {\n url: 'https://api.sandbox.paypal.com/v1/vault/credit-cards/',\n method: 'POST',\n headers: {\n content_type: 'application/json',\n authorization: `Bearer ${token}`\n },\n 'payload':{\n \"number\": \"4417119669820331\",\n \"type\": \"visa\",\n \"expire_month\": \"11\",\n \"expire_year\": \"2024\",\n \"first_name\": \"Joe\",\n \"last_name\": \"Shopper\",\n \"billing_address\": {\n \"line1\": \"52 N Main St.\",\n \"city\": \"Johnstown\",\n \"country_code\": \"US\",\n \"postal_code\": \"43210\",\n \"state\": \"OH\",\n \"phone\": \"408-334-8890\"\n }\n }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":140,"wires":[["23fd9bb05a9e0890"]]},{"id":"ea5f619acb7c6975","type":"debug","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":950,"y":160,"wires":[]},{"id":"c6338a46953ce90f","type":"inject","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":140,"wires":[["56941dc7fa2c9109"]]},{"id":"0f0959f25b8ffac2","type":"function","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"set token","func":"global.set('token',msg.oauth2Response.access_token);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":700,"y":120,"wires":[["e398f17b55741fad"]]},{"id":"d501a2a68aed65ff","type":"oauth2","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","container":"oauth2Response","grant_type":"client_credentials","access_token_url":"https://api.sandbox.paypal.com/v1/oauth2/token","username":"","password":"","client_id":"ATg1CMHw7GDbOH-XMd_IgzoksBNlvO_ieIyR-oBMoM_xiewyI7RrK8EOLMj438TAchfYtnVD6YeICHu2","client_secret":"EP2Xdc1ni1YGoT8EMiYPWQHhUDLrPM4oOitsEJShS4zmSKIEX0nfArtDtauxxhgnJd7BxLyOfEoIjow6","scope":"","headers":{},"x":450,"y":60,"wires":[["0f0959f25b8ffac2"]]},{"id":"74cb073642513c64","type":"inject","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":60,"wires":[["d501a2a68aed65ff"]]},{"id":"b438631713b789b3","type":"inject","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":220,"wires":[["ba9065365539726b"]]},{"id":"ba9065365539726b","type":"function","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"POST orders","func":"let token = global.get('token');\nmsg = {\n url: 'https://api-m.sandbox.paypal.com/v2/checkout/orders',\n method: 'POST',\n headers: {\n 'Content-type': 'application/json',\n 'Authorization': `Bearer ${token}`\n },\n 'payload':{\n \"intent\": \"CAPTURE\",\n \"purchase_units\": [\n {\n \"amount\": {\n \"currency_code\": \"USD\",\n \"value\": \"100.00\"\n }\n }\n ]\n }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":220,"wires":[["23fd9bb05a9e0890"]]},{"id":"7ae923df938ff7c2","type":"inject","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":260,"wires":[["2c022952654e4a88"]]},{"id":"2c022952654e4a88","type":"function","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"GET invoices","func":"let token = global.get('token');\nmsg = {\n url: 'https://api-m.sandbox.paypal.com/v2/invoicing/invoices?total_required=true',\n method: 'GET',\n headers: {\n content_type: 'application/json',\n authorization: `Bearer ${token}`\n }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":260,"wires":[["23fd9bb05a9e0890"]]},{"id":"7b99c317fa69b979","type":"function","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"POST generate-next-invoice-number","func":"let token = global.get('token');\nmsg = {\n url: 'https://api-m.sandbox.paypal.com/v2/invoicing/generate-next-invoice-number',\n method: 'POST',\n headers: {\n content_type: 'application/json',\n authorization: `Bearer ${token}`\n }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":180,"wires":[["23fd9bb05a9e0890"]]},{"id":"50f21b0248ad11fa","type":"inject","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":180,"wires":[["7b99c317fa69b979"]]},{"id":"1e9fe7a88f8a9fd5","type":"function","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"GET webhooks","func":"let token = global.get('token');\nmsg = {\n url: 'https://api-m.sandbox.paypal.com/v1/notifications/webhooks',\n method: 'GET',\n headers: {\n content_type: 'application/json',\n authorization: `Bearer ${token}`\n }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":300,"wires":[["23fd9bb05a9e0890"]]},{"id":"9f0816024a976737","type":"inject","z":"3db3b0fd97716db6","g":"77cb8766f3d0f394","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":300,"wires":[["1e9fe7a88f8a9fd5"]]},{"id":"4a659fbacbd5a9ba","type":"http in","z":"3db3b0fd97716db6","g":"a5913a06e02ebba5","name":"","url":"/webhook","method":"post","upload":false,"swaggerDoc":"","x":380,"y":440,"wires":[["f7a09a555df4434c"]]},{"id":"f7a09a555df4434c","type":"function","z":"3db3b0fd97716db6","g":"a5913a06e02ebba5","name":"x-real-ip","func":"let ips = global.get('x-ip') || {};\n\nif ( msg.req.headers.host != \"localhost:1880\") {\n \n if (msg.req.headers[\"x-real-ip\"] in ips){\n ips[msg.req.headers[\"x-real-ip\"]] = ips[msg.req.headers[\"x-real-ip\"]] + 1;\n } else {\n ips[msg.req.headers[\"x-real-ip\"]] = 1;\n }\n \n global.set('X-Forwarded-For', msg.req.headers[\"X-Forwarded-For\"]);\n global.set('x-ip', ips);\n \n msg.payload = msg.req.headers[\"x-real-ip\"];\n msg.ip = msg.req.headers[\"x-real-ip\"];\n \n return msg;\n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":440,"wires":[["c22f0bf46fb98541"]],"icon":"node-red/alert.svg"},{"id":"4eb9ce10f8328334","type":"http response","z":"3db3b0fd97716db6","g":"a5913a06e02ebba5","name":"","statusCode":"200","headers":{},"x":920,"y":440,"wires":[]},{"id":"ba768d993577dd9a","type":"debug","z":"3db3b0fd97716db6","g":"a5913a06e02ebba5","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":910,"y":400,"wires":[]}] -------------------------------------------------------------------------------- /examples/node-red-contrib-oauth2.json: -------------------------------------------------------------------------------- 1 | [{"id":"d15d33a4fa9a8ad9","type":"tab","label":"node-red-contrib-oauth2","disabled":false,"info":"","env":[]},{"id":"368ef084fe6b53de","type":"group","z":"d15d33a4fa9a8ad9","name":"Grant Type","style":{"label":true,"fill":"#bfc7d7","color":"#0070c0"},"nodes":["7e1a2f9059f99060","54025cb0a19e0f14","f37b3410d0e03c57","a783ce686ac81a0c"],"x":48,"y":33,"w":1276,"h":760},{"id":"7e1a2f9059f99060","type":"group","z":"d15d33a4fa9a8ad9","g":"368ef084fe6b53de","name":"REFRESH","style":{"label":true,"stroke":"#7f7f7f","color":"#000000","fill":"#bfdbef"},"nodes":["d1ba341040cea0c3","efd1a2a910ca1051","a8fca004e704e071","9e9332c792dbdc01","7f22873286146e83","950f5dc3363c0b75","7eca8d657ce494b0","ec929c9a7e6cd3fa","f8505333b297b225","2b42ac78e4a9307d"],"x":74,"y":499,"w":1224,"h":268},{"id":"54025cb0a19e0f14","type":"group","z":"d15d33a4fa9a8ad9","g":"368ef084fe6b53de","name":"Client Credentials","style":{"label":true,"stroke":"#a4a4a4","color":"#ffffff","fill":"#3f93cf"},"nodes":["10de26226e66ea9e","147aae5c70a7eecb","1e2169485cd2be43","ca6a8cddea946ab2","c6d4eb5c867902eb","d7a2041ef2683870","3f5a09c4fc8e769f"],"x":74,"y":179,"w":1212,"h":142},{"id":"f37b3410d0e03c57","type":"group","z":"d15d33a4fa9a8ad9","g":"368ef084fe6b53de","name":"Authorization Code","style":{"label":true,"fill":"#3f93cf","color":"#ffffff"},"nodes":["8bd177259bc473f4","ee86852f62926632","d340377991d2d9b9","9e4d3edfde1766b9"],"x":74,"y":59,"w":1212,"h":82},{"id":"a783ce686ac81a0c","type":"group","z":"d15d33a4fa9a8ad9","g":"368ef084fe6b53de","name":"Password","style":{"label":true,"fill":"#3f93cf","color":"#ffffff"},"nodes":["5ff7fb2616706080","8eadc61e4e5b4ac2","d857d0bc3e90d394","95745c4253c151f0","dcbde702eae7191d","d6a9695a923ab18b","c5da03044bdca6f1"],"x":74,"y":339,"w":1212,"h":142},{"id":"7f22873286146e83","type":"group","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"via HTTP REQUEST","style":{"label":true},"nodes":["64d9243f.b7be7c","31727a2c.05a026","4030c52c.c2c29c","3b67977c.a53c08"],"x":104,"y":659,"w":1168,"h":82},{"id":"ee86852f62926632","type":"oauth2","z":"d15d33a4fa9a8ad9","g":"f37b3410d0e03c57","name":"Authorization Code","container":"oauth2Response","grant_type":"refresh_token","access_token_url":"http://localhost:8088/v1/oauth/tokens","authorization_endpoint":"http://localhost:8088/web/authorize","redirect_uri":"http://localhost:1880/oauth2/redirect","open_authentication":"1a4132fe-b0c6-4f0b-9e3c-ef40d5f6ef90","username":"","password":"","client_id":"test_client_1","client_secret":"test_secret","response_type":"","access_type":"","prompt":"","scope":"read_write","resource":"","state":"","proxy":"","debug":true,"senderr":true,"client_credentials_in_body":false,"rejectUnauthorized":false,"headers":{},"x":450,"y":100,"wires":[["8bd177259bc473f4"]]},{"id":"9e4d3edfde1766b9","type":"inject","z":"d15d33a4fa9a8ad9","g":"f37b3410d0e03c57","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":100,"wires":[["ee86852f62926632"]]},{"id":"d7a2041ef2683870","type":"debug","z":"d15d33a4fa9a8ad9","g":"54025cb0a19e0f14","name":"DBG 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1190,"y":220,"wires":[]},{"id":"c6d4eb5c867902eb","type":"oauth2","z":"d15d33a4fa9a8ad9","g":"54025cb0a19e0f14","name":"Client Credentials","container":"oauth2Response","grant_type":"client_credentials","access_token_url":"http://localhost:8088/v1/oauth/tokens","authorization_endpoint":"http://localhost:8080/web/authorize","redirect_uri":"http://localhost:1880/oauth2/redirect","open_authentication":"41595d3b-a2b5-41aa-84e7-02d2c7b54304","username":"","password":"","client_id":"test_client_1","client_secret":"test_secret","response_type":"","access_type":"","prompt":"","scope":"read_write","resource":"","state":"","proxy":"","debug":true,"senderr":true,"client_credentials_in_body":false,"rejectUnauthorized":false,"headers":{},"x":450,"y":220,"wires":[["1e2169485cd2be43"]]},{"id":"3f5a09c4fc8e769f","type":"inject","z":"d15d33a4fa9a8ad9","g":"54025cb0a19e0f14","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":220,"wires":[["c6d4eb5c867902eb"]]},{"id":"d1ba341040cea0c3","type":"inject","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"Refresh Token","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"3300","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":540,"wires":[["efd1a2a910ca1051"]]},{"id":"efd1a2a910ca1051","type":"oauth2","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"Refresh Token","container":"oauth2Response","grant_type":"refresh_token","access_token_url":"http://localhost:8088/v1/oauth/tokens","authorization_endpoint":"","redirect_uri":"http://localhost:1880/oauth2/redirect","open_authentication":"","username":"pedroet","password":"","client_id":"test_client_1","client_secret":"test_secret","response_type":"","access_type":"","refresh_token":"b44b45c2-bc7e-4c8f-859b-6890d9d22eeb","prompt":"","scope":"read_write","resource":"","state":"","proxy":"","debug":false,"senderr":false,"client_credentials_in_body":false,"rejectUnauthorized":false,"headers":{},"x":700,"y":540,"wires":[["a8fca004e704e071"]]},{"id":"a8fca004e704e071","type":"function","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"Set refreshToken","func":"if (msg.oauth2Response.refresh_token) {\n global.set('refreshToken', msg.oauth2Response.refresh_token);\n}\nif (msg.oauth2Response.access_token) {\n global.set('accessToken', msg.oauth2Response.access_token);\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":978,"y":540,"wires":[["9e9332c792dbdc01"]]},{"id":"9e9332c792dbdc01","type":"debug","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"DBG 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1187,"y":540,"wires":[]},{"id":"4030c52c.c2c29c","type":"inject","z":"d15d33a4fa9a8ad9","g":"7f22873286146e83","name":"Refresh Token","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":230,"y":700,"wires":[["31727a2c.05a026"]]},{"id":"31727a2c.05a026","type":"function","z":"d15d33a4fa9a8ad9","g":"7f22873286146e83","name":"SETTING REQUEST","func":"var REFRESH_TOKEN = global.get('refreshToken');\nvar ACCESS_TOKEN = global.get('accessToken');\n\nmsg.method = \"POST\"\nmsg.url = `http://localhost:8088/v1/oauth/tokens?grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}`\n\nmsg.headers = {}\nmsg.headers[\"content-type\"] = \"application/json\"\nmsg.headers[\"Authorization\"] = 'Basic ' + Buffer.from('test_client_1:test_secret').toString('base64');\n\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":452,"y":700,"wires":[["64d9243f.b7be7c"]]},{"id":"64d9243f.b7be7c","type":"http request","z":"d15d33a4fa9a8ad9","g":"7f22873286146e83","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":810,"y":700,"wires":[["3b67977c.a53c08"]]},{"id":"3b67977c.a53c08","type":"debug","z":"d15d33a4fa9a8ad9","g":"7f22873286146e83","name":"DBG 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1176,"y":700,"wires":[]},{"id":"1e2169485cd2be43","type":"function","z":"d15d33a4fa9a8ad9","g":"54025cb0a19e0f14","name":"Set refreshToken","func":"if (msg.oauth2Response.refresh_token) {\n global.set('refreshToken', msg.oauth2Response.refresh_token);\n}\nif (msg.oauth2Response.access_token) {\n global.set('accessToken', msg.oauth2Response.access_token);\n}\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":220,"wires":[["d7a2041ef2683870"]]},{"id":"147aae5c70a7eecb","type":"function","z":"d15d33a4fa9a8ad9","g":"54025cb0a19e0f14","name":"set msg.oauth2Request","func":"msg.oauth2Request = { \n \"access_token_url\": \"http://localhost:8088/v1/oauth/tokens\",\n \"credentials\": {\n \"grant_type\": \"client_credentials\",\n \"client_id\": \"test_client_1\",\n \"client_secret\": \"test_secret\",\n \"scope\": \"read_write\"\n },\n};\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":280,"wires":[["10de26226e66ea9e"]]},{"id":"10de26226e66ea9e","type":"oauth2","z":"d15d33a4fa9a8ad9","g":"54025cb0a19e0f14","name":"Set by msg.oauth2Request","container":"oauth2Response","grant_type":"set_by_credentials","access_token_url":"http://localhost:3000/oauth/token ","authorization_endpoint":"","redirect_uri":"http://localhost:1880/oauth2/redirect","open_authentication":"","username":"pedroet","password":"","client_id":"confidentialApplication","client_secret":"topSecret","response_type":"","access_type":"","prompt":"","scope":"*","resource":"","state":"","proxy":"","senderr":false,"client_credentials_in_body":false,"rejectUnauthorized":false,"headers":{},"x":720,"y":280,"wires":[["1e2169485cd2be43"]]},{"id":"ca6a8cddea946ab2","type":"inject","z":"d15d33a4fa9a8ad9","g":"54025cb0a19e0f14","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":280,"wires":[["147aae5c70a7eecb"]]},{"id":"8bd177259bc473f4","type":"function","z":"d15d33a4fa9a8ad9","g":"f37b3410d0e03c57","name":"Set refreshToken","func":"if (msg.oauth2Response.refresh_token) {\n global.set('refreshToken', msg.oauth2Response.refresh_token);\n}\nif (msg.oauth2Response.access_token) {\n global.set('accessToken', msg.oauth2Response.access_token);\n}\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":100,"wires":[["d340377991d2d9b9"]]},{"id":"d340377991d2d9b9","type":"debug","z":"d15d33a4fa9a8ad9","g":"f37b3410d0e03c57","name":"DBG 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1190,"y":100,"wires":[]},{"id":"5ff7fb2616706080","type":"oauth2","z":"d15d33a4fa9a8ad9","g":"a783ce686ac81a0c","name":"Set by msg.oauth2Request","container":"oauth2Response","grant_type":"set_by_credentials","access_token_url":"http://localhost:3000/oauth/token ","authorization_endpoint":"","redirect_uri":"http://localhost:1880/oauth2/redirect","open_authentication":"","username":"pedroet","password":"","client_id":"confidentialApplication","client_secret":"topSecret","response_type":"","access_type":"","prompt":"","scope":"*","resource":"","state":"","proxy":"","debug":false,"senderr":false,"client_credentials_in_body":false,"rejectUnauthorized":false,"headers":{},"x":720,"y":440,"wires":[["d857d0bc3e90d394"]]},{"id":"8eadc61e4e5b4ac2","type":"function","z":"d15d33a4fa9a8ad9","g":"a783ce686ac81a0c","name":"set msg.oauth2Request","func":"msg.oauth2Request = {\n \"access_token_url\": \"http://localhost:8088/v1/oauth/tokens\",\n \"credentials\": {\n \"grant_type\": \"password\",\n \"client_id\": \"test_client_1\",\n \"client_secret\": \"test_secret\",\n \"scope\": \"read_write\",\n \"username\": \"test@user\",\n \"password\": \"test_password\"\n },\n};\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":440,"wires":[["5ff7fb2616706080"]]},{"id":"d857d0bc3e90d394","type":"function","z":"d15d33a4fa9a8ad9","g":"a783ce686ac81a0c","name":"Set refreshToken","func":"if (msg.oauth2Response.refresh_token) {\n global.set('refreshToken', msg.oauth2Response.refresh_token);\n}\nif (msg.oauth2Response.access_token) {\n global.set('accessToken', msg.oauth2Response.access_token);\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":990,"y":380,"wires":[["dcbde702eae7191d"]]},{"id":"95745c4253c151f0","type":"inject","z":"d15d33a4fa9a8ad9","g":"a783ce686ac81a0c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":440,"wires":[["8eadc61e4e5b4ac2"]]},{"id":"dcbde702eae7191d","type":"debug","z":"d15d33a4fa9a8ad9","g":"a783ce686ac81a0c","name":"DBG 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1190,"y":380,"wires":[]},{"id":"d6a9695a923ab18b","type":"inject","z":"d15d33a4fa9a8ad9","g":"a783ce686ac81a0c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":380,"wires":[["c5da03044bdca6f1"]]},{"id":"c5da03044bdca6f1","type":"oauth2","z":"d15d33a4fa9a8ad9","g":"a783ce686ac81a0c","name":"Password","container":"oauth2Response","grant_type":"password","access_token_url":"http://localhost:8088/v1/oauth/tokens","authorization_endpoint":"http://localhost:8080/web/authorize","redirect_uri":"http://localhost:1880/oauth2/redirect","open_authentication":"41595d3b-a2b5-41aa-84e7-02d2c7b54304","username":"test@user","password":"test_password","client_id":"test_client_1","client_secret":"test_secret","response_type":"","access_type":"","prompt":"","scope":"read_write","resource":"","state":"","proxy":"","senderr":true,"client_credentials_in_body":false,"rejectUnauthorized":false,"headers":{},"x":480,"y":380,"wires":[["d857d0bc3e90d394"]]},{"id":"950f5dc3363c0b75","type":"oauth2","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"Set by msg.oauth2Request","container":"oauth2Response","grant_type":"set_by_credentials","access_token_url":"http://localhost:3000/oauth/token ","authorization_endpoint":"","redirect_uri":"http://localhost:1880/oauth2/redirect","open_authentication":"","username":"pedroet","password":"","client_id":"confidentialApplication","client_secret":"topSecret","response_type":"","access_type":"","prompt":"","scope":"*","resource":"","state":"","proxy":"","senderr":true,"client_credentials_in_body":false,"rejectUnauthorized":false,"headers":{},"x":740,"y":600,"wires":[["ec929c9a7e6cd3fa"]]},{"id":"7eca8d657ce494b0","type":"function","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"set msg.oauth2Request","func":"let refreshToken = global.get('refreshToken');\n\nmsg.oauth2Request = { \n \"access_token_url\": \"http://localhost:8088/v1/oauth/tokens\",\n \"credentials\": {\n \"grant_type\": \"refresh_token\",\n \"client_id\": \"test_client_1\",\n \"client_secret\": \"test_secret\",\n \"scope\": \"read_write\",\n \"refresh_token\": refreshToken \n },\n};\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":600,"wires":[["950f5dc3363c0b75"]]},{"id":"ec929c9a7e6cd3fa","type":"function","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"Set refreshToken","func":"if (msg.oauth2Response.refresh_token) {\n global.set('refreshToken', msg.oauth2Response.refresh_token);\n}\nif (msg.oauth2Response.access_token) {\n global.set('accessToken', msg.oauth2Response.access_token);\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":980,"y":600,"wires":[["2b42ac78e4a9307d"]]},{"id":"f8505333b297b225","type":"inject","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"Refresh Token","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"3300","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":600,"wires":[["7eca8d657ce494b0"]]},{"id":"2b42ac78e4a9307d","type":"debug","z":"d15d33a4fa9a8ad9","g":"7e1a2f9059f99060","name":"DBG 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1190,"y":600,"wires":[]}] -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": ["./"], 8 | "exclude": ["node_modules", "docs"], 9 | "includePattern": ".+\\.js(doc|x)?$", 10 | "excludePattern": "(^|\\/|\\\\)_" 11 | }, 12 | "opts": { 13 | "encoding": "utf8", 14 | "destination": "./docs/", 15 | "recurse": true, 16 | "verbose": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-oauth2", 3 | "version": "6.2.1", 4 | "description": "The node-red-contrib-oauth2 is a Node-RED node that provides an OAuth2 authentication flow. This node uses the OAuth2 protocol to obtain an access token, which can be used to make authenticated API requests.", 5 | "author": "Marcos Caputo ", 6 | "contributors": [ 7 | "Nariyuki Saito ", 8 | "Michael Sommer ", 9 | "Emanuel Miron", 10 | "serotonie", 11 | "deosrc" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/caputomarcos/node-red-contrib-oauth2#readme", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/caputomarcos/node-red-contrib-oauth2.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/caputomarcos/node-red-contrib-oauth2/issues" 21 | }, 22 | "engines": { 23 | "node": ">=12.0.0" 24 | }, 25 | "keywords": [ 26 | "node-red", 27 | "oauth2", 28 | "security" 29 | ], 30 | "node-red": { 31 | "version": ">=3.0.0", 32 | "nodes": { 33 | "oauth2": "src/oauth2.js" 34 | } 35 | }, 36 | "resolutions": { 37 | "eslint": "^8.0.1" 38 | }, 39 | "dependencies": { 40 | "axios": "^1.7.2", 41 | "mocha": "^10.4.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/preset-env": "^7.24.5", 45 | "eslint-config-prettier": "^9.1.0", 46 | "eslint-plugin-import": "^2.29.1", 47 | "eslint-plugin-jsdoc": "^48.2.6", 48 | "eslint-plugin-mocha": "^10.4.3", 49 | "eslint-plugin-n": "^17.7.0", 50 | "eslint-plugin-prettier": "^5.1.3", 51 | "eslint-plugin-promise": "^6.1.1", 52 | "jsdoc": "^4.0.3", 53 | "json-schema": ">=0.4.0", 54 | "nock": "^13.5.4", 55 | "node-red": "^3.1.9", 56 | "node-red-node-test-helper": "^0.3.4", 57 | "nyc": "^15.1.0", 58 | "prettier": "^3.2.5", 59 | "should": "^13.2.3", 60 | "eslint": "^8.0.1", 61 | "eslint-config-standard": "^17.1.0", 62 | "@babel/eslint-parser": "^7.24.5" 63 | }, 64 | "eslintConfig": { 65 | "extends": "./.eslintrc.yml" 66 | }, 67 | "scripts": { 68 | "fix": "prettier --plugin-search-dir . --write ./src/. && npx eslint ./src/. --fix", 69 | "lint": "prettier --plugin-search-dir . --check ./src/. && npx eslint ./src/.", 70 | "format": "prettier --plugin-search-dir . --write ./src/.", 71 | "doc": "jsdoc -c jsdoc.json", 72 | "test1": "mocha \"test/**/*_spec.js\"", 73 | "test": "mocha --recursive --async-stack-traces --full-trace", 74 | "coverage": "nyc npm test && nyc report --reporter=text-summary --reporter=html" 75 | }, 76 | "nyc": { 77 | "reporter": [ 78 | "html", 79 | "text" 80 | ], 81 | "report-dir": "./coverage" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/icons/oauth2.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/libs/logger.js: -------------------------------------------------------------------------------- 1 | const loggerStatusActive = { fill: 'yellow', shape: 'ring', text: 'Logging' }; 2 | const loggerStatusOff = { fill: 'green', shape: 'ring', text: '' }; 3 | const { inspect } = require('util'); 4 | 5 | function Logger(label = '***', active = true, count = null, msg) { 6 | this.consoleFunction = console.log; 7 | this.sendFunction = this.sendConsole; 8 | this.type = 'debug'; 9 | if (label instanceof Object) Object.assign(this, label); 10 | else this.label = label; 11 | if (this.active == undefined) this.active = active; 12 | if (this.count == undefined) this.count = count; 13 | this.set(this.active, this.count); 14 | return msg ? this.sendInfo(msg) : this; 15 | } 16 | Logger.prototype.objectDump = function (o) { 17 | if (typeof o !== 'object' || o === null) { 18 | this.send('Invalid object', 'error'); 19 | return this; 20 | } 21 | this.send(inspect(o, { showHidden: true, depth: null }), 'info'); 22 | return this; 23 | }; 24 | 25 | Logger.prototype.send = function (message, type, node, sendFunction = this.sendFunction) { 26 | if (!this.active || !type || message == null) return this; 27 | 28 | const sendMessage = (msg) => { 29 | try { 30 | sendFunction.call(this, msg instanceof Object ? JSON.stringify(msg) : msg, type, node); 31 | } catch (ex) { 32 | sendFunction.call(this, ex.message, type, node); 33 | } 34 | }; 35 | 36 | if (!this.count) { 37 | sendMessage(message); 38 | } 39 | 40 | if (this.count > 0) { 41 | sendMessage(message); 42 | if (--this.count === 0) { 43 | this.setOff(); 44 | } 45 | } 46 | 47 | return this; 48 | }; 49 | 50 | Logger.prototype.sendConsole = function (message, type = this.type, consoleFunction = this.consoleFunction) { 51 | const ts = new Date().toString().split(' '); 52 | consoleFunction.apply(this, [[parseInt(ts[2], 10), ts[1], ts[4]].join(' ') + ' - [' + type + '] ' + this.label + ' ' + message]); 53 | return this; 54 | }; 55 | Logger.prototype.sendDebug = function (message, values) { 56 | if (values) return this.send(Object.assign({}, { label: message }, values), 'debug'); 57 | return this.send(message, 'debug'); 58 | }; 59 | Logger.prototype.debug = Logger.prototype.sendDebug; 60 | Logger.prototype.sendError = function (message) { 61 | return this.send(message, 'error'); 62 | }; 63 | Logger.prototype.error = Logger.prototype.sendError; 64 | Logger.prototype.sendErrorAndDump = function (message, o, ex) { 65 | return this.sendError(message).objectDump(o).stackDump(ex); 66 | }; 67 | Logger.prototype.sendErrorAndStackDump = function (message, ex) { 68 | return this.sendError(message).stackDump(ex); 69 | }; 70 | Logger.prototype.sendInfo = function (message) { 71 | return this.send(message, 'info'); 72 | }; 73 | Logger.prototype.info = Logger.prototype.sendInfo; 74 | Logger.prototype.sendNode = function (message, node = this.node, type = this.noderedLogType) { 75 | return node[type](message); 76 | }; 77 | Logger.prototype.sendWarn = function (message) { 78 | return this.send(message, 'warn'); 79 | }; 80 | Logger.prototype.sendWarning = Logger.prototype.sendWarn; 81 | Logger.prototype.warn = Logger.prototype.sendWarn; 82 | Logger.prototype.warning = Logger.prototype.sendWarn; 83 | Logger.prototype.setNodeStatus = function (node) { 84 | if (node) this.node = node; 85 | if (this.node == null) throw Error('No node set'); 86 | this.showNodeStatus(); 87 | return this; 88 | }; 89 | Logger.prototype.set = function (active, count) { 90 | if (count !== undefined) { 91 | this.count = count; 92 | this.countDefault = count; 93 | } 94 | if (active !== undefined) this.active = active; 95 | this.showNodeStatus(); 96 | // this.sendConsole('logging turning ' + (this.active ? 'on logging next ' + this.count + ' log points' : 'off')); 97 | return this; 98 | }; 99 | Logger.prototype.setOff = function () { 100 | return this.set(false); 101 | }; 102 | Logger.prototype.setOn = function (count = this.countDefault) { 103 | return this.set(true, count); 104 | }; 105 | Logger.prototype.showNodeStatus = function () { 106 | if (this.node) this.node.status(this.active ? loggerStatusActive : loggerStatusOff); 107 | }; 108 | Logger.prototype.stackDump = function (ex) { 109 | if (ex) console.log(ex.stack); 110 | else console.trace(); 111 | return this; 112 | }; 113 | 114 | module.exports = Logger; 115 | -------------------------------------------------------------------------------- /src/locales/en-US/oauth2.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 166 | -------------------------------------------------------------------------------- /src/locales/en-US/oauth2.json: -------------------------------------------------------------------------------- 1 | { 2 | "oauth2": { 3 | "oauth2": "oauth2", 4 | "tips": { 5 | "debug": "Check the box to enable debug mode with extra logs.", 6 | "client_credentials_in_body": "Ensure that the client credentials are included in the token request body for authentication purposes.", 7 | "rejectUnauthorized": "The rejectUnauthorized parameter controls SSL/TLS certificate validation for the server, with true enforcing validation and false disabling it.", 8 | "force": "Use the Force option to generate a new access token even if the current token is still valid. This is useful for ensuring that your token is always fresh, especially if there are changes in scopes or permissions, or if you encounter authentication issues." 9 | }, 10 | "label": { 11 | "debug": "Debug Mode.", 12 | "grant_type": "Grant Type", 13 | "name": "Name", 14 | "container": "Container", 15 | "access_token_url": "Access Token URL", 16 | "authorization_endpoint": "Authorization Endpoint", 17 | "redirect_uri": "Redirect URI", 18 | "open_authentication": "Code", 19 | "username": "Username", 20 | "password": "Password", 21 | "client_id": "Client ID", 22 | "client_secret": "Client Secret", 23 | "access_type": "Access Type", 24 | "response_type": "Response Type", 25 | "prompt": "Prompt", 26 | "scope": "Scope", 27 | "resource": "Resource", 28 | "state": "State", 29 | "rejectUnauthorized": "Reject Unauthorized", 30 | "client_credentials_in_body": "Embedded Credentials", 31 | "proxy-config": "Proxy Configuration", 32 | "use-proxy": "Use proxy", 33 | "senderr": "Only send non-2xx responses to Catch node", 34 | "refresh_token": "Refresh Token", 35 | "force": "Force Token Refresh" 36 | }, 37 | "placeholder": { 38 | "name": "oauth2", 39 | "container": "oauth2Response", 40 | "access_token_url": "https://github.com/login/oauth/access_token", 41 | "authorization_endpoint": "https://github.com/login/oauth/authorize", 42 | "redirect_uri": "/oauth2/redirect", 43 | "open_authentication": "1b897416-3de3-4631-a0e9-7f7431e1a959", 44 | "username": "admin", 45 | "password": "admin", 46 | "client_id": "012493af6282be51660dbc8e21a8462e", 47 | "client_secret": "5621bd4b5a8b09ed31817efb8d54fda2c72bfc1c6968cd4563d83f7cc26f68f6", 48 | "access_type": "offline", 49 | "response_type": "code", 50 | "prompt": "consent", 51 | "scope": "scope", 52 | "resource": "resource", 53 | "state": "state", 54 | "rejectUnauthorized": "rejectUnauthorized", 55 | "headers": "headers", 56 | "refresh_token": "refresh_token" 57 | }, 58 | "opts": { 59 | "client_credentials": "Client Credentials", 60 | "password_credentials": "Password", 61 | "authorization_code": "Authorization Code", 62 | "implicit_flow": "Implicit Flow", 63 | "refresh_token": "Refresh Token", 64 | "set_by_credentials": "- Set by msg.oauth2Request -" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/oauth2.js: -------------------------------------------------------------------------------- 1 | module.exports = function (RED) { 2 | 'use strict'; 3 | 4 | const axios = require('axios'); 5 | const http = require('http'); 6 | const https = require('https'); 7 | const { URLSearchParams } = require('url'); 8 | const Logger = require('node-red-contrib-oauth2/src/libs/logger'); 9 | 10 | /** 11 | * Class representing an OAuth2 Node. 12 | */ 13 | class OAuth2Node { 14 | /** 15 | * Create an OAuth2Node. 16 | * @param {Object} config - Node configuration object. 17 | */ 18 | constructor(config) { 19 | RED.nodes.createNode(this, config); 20 | this.logger = new Logger({ name: 'identifier', count: null, active: config.debug || false, label: 'debug' }); 21 | this.logger.debug('Constructor: Initializing node with config', config); 22 | 23 | // Node configuration properties 24 | this.name = config.name || ''; 25 | this.container = config.container || ''; 26 | this.access_token_url = config.access_token_url || ''; 27 | this.authorization_endpoint = config.authorization_endpoint || ''; 28 | this.redirect_uri = config.redirect_uri || ''; 29 | this.grant_type = config.grant_type || ''; 30 | this.refresh_token = config.refresh_token || ''; 31 | this.username = config.username || ''; 32 | this.password = config.password || ''; 33 | this.client_id = config.client_id || ''; 34 | this.client_secret = config.client_secret || ''; 35 | this.scope = config.scope || ''; 36 | this.resource = config.resource || ''; 37 | this.state = config.state || ''; 38 | this.rejectUnauthorized = config.rejectUnauthorized || false; 39 | this.client_credentials_in_body = config.client_credentials_in_body || false; 40 | this.headers = config.headers || {}; 41 | this.sendErrorsToCatch = config.senderr || false; 42 | this.proxy = config.proxy || false; 43 | this.force = config.force || false; 44 | this.logger.debug('Constructor: Finished setting up node properties'); 45 | 46 | // Register the input handler 47 | this.on('input', this.onInput.bind(this)); 48 | this.host = RED.settings.uiHost || 'localhost'; 49 | this.logger.debug('Constructor: Node input handler registered'); 50 | 51 | // Load credentials from Node-RED 52 | this.credentials = RED.nodes.getCredentials(this.id) || {}; 53 | } 54 | 55 | /** 56 | * Handles input messages. 57 | * @param {Object} msg - Input message object. 58 | * @param {Function} send - Function to send messages. 59 | * @param {Function} done - Function to indicate processing is complete. 60 | */ 61 | async onInput(msg, send, done) { 62 | this.logger.debug('onInput: Received message', msg); 63 | 64 | // Check if access token is stored and still valid 65 | if (!this.force && this.credentials.access_token && this.credentials.expire_time) { 66 | const currentTime = Math.floor(Date.now() / 1000); 67 | if (currentTime < this.credentials.expire_time) { 68 | this.logger.debug('onInput: Using stored access token'); 69 | msg = { ...msg, oauth2Response: this.credentials.oauth2Response, headers: this.credentials.headers }; 70 | this.setStatus('green', 'Access token still valid!'); 71 | send(msg); 72 | done(); 73 | return; 74 | } 75 | } 76 | 77 | const options = this.generateOptions(msg); // Generate request options 78 | this.logger.debug('onInput: Generated request options', options); 79 | 80 | this.configureProxy(); // Configure proxy settings 81 | this.logger.debug('onInput: Configured proxy settings', this.prox); 82 | 83 | delete msg.oauth2Request; // Remove oauth2Request from msg 84 | this.logger.debug('onInput: Removed oauth2Request from message'); 85 | 86 | options.form = this.cleanForm(options.form); // Clean the form data 87 | this.logger.debug('onInput: Cleaned form data', options.form); 88 | 89 | try { 90 | const response = await this.makePostRequest(options); // Make the POST request 91 | this.logger.debug('onInput: POST request response', response); 92 | this.handleResponse(response, msg, send); // Handle the response 93 | } catch (error) { 94 | this.logger.error('onInput: Error making POST request', error); 95 | this.handleError(error, msg, send); // Handle any errors 96 | } 97 | 98 | done(); // Indicate that processing is complete 99 | this.logger.debug('onInput: Finished processing input'); 100 | } 101 | 102 | /** 103 | * Generates options for the HTTP request. 104 | * @param {Object} msg - Input message object. 105 | * @returns {Object} - The request options. 106 | */ 107 | generateOptions(msg) { 108 | // Log the start of the option generation process with the input message 109 | this.logger.debug('generateOptions: Configuring options with message', msg); 110 | 111 | // Initialize the form object to hold the form data 112 | let form = {}; 113 | // Set the default URL to the access token URL configured in the node 114 | let url = this.access_token_url; 115 | // Initialize headers with default Content-Type and Accept headers 116 | let headers = { 117 | 'Content-Type': 'application/x-www-form-urlencoded', 118 | Accept: 'application/json' 119 | }; 120 | 121 | // Retrieve credentials from the message if available, otherwise use an empty object 122 | const creds = msg.oauth2Request ? msg.oauth2Request.credentials || {} : {}; 123 | // Initialize the form data with grant_type, scope, resource, and state 124 | form = { 125 | grant_type: creds.grant_type || this.grant_type, 126 | scope: creds.scope || this.scope, 127 | resource: creds.resource || this.resource, 128 | state: creds.state || this.state 129 | }; 130 | 131 | // Define functions for different OAuth2 flows 132 | const flows = { 133 | // Password flow function 134 | password: () => { 135 | this.logger.debug('generateOptions: Password flow detected'); 136 | form.username = creds.username || this.username; 137 | form.password = creds.password || this.password; 138 | }, 139 | // Client credentials flow function 140 | client_credential: () => { 141 | this.logger.debug('generateOptions: Client credentials flow detected'); 142 | form.client_id = creds.client_id || this.client_id; 143 | form.client_secret = creds.client_secret || this.client_secret; 144 | }, 145 | // Refresh token flow function 146 | refresh_token: () => { 147 | this.logger.debug('generateOptions: Refresh token flow detected'); 148 | form.client_id = creds.client_id || this.client_id; 149 | form.client_secret = creds.client_secret || this.client_secret; 150 | form.refresh_token = creds.refresh_token || this.refresh_token; 151 | }, 152 | // Authorization code flow function 153 | authorization_code: () => { 154 | this.logger.debug('generateOptions: Authorization code flow detected'); 155 | const credentials = RED.nodes.getCredentials(this.id) || {}; 156 | if (credentials) { 157 | form.code = credentials.code; 158 | form.redirect_uri = this.redirect_uri; 159 | } 160 | }, 161 | // Implicit flow function 162 | implicit_flow: () => { 163 | this.logger.debug('generateOptions: Implicit flow detected'); 164 | const credentials = RED.nodes.getCredentials(this.id) || {}; 165 | if (credentials) { 166 | form.client_id = this.client_id; 167 | form.client_secret = this.client_secret; 168 | form.code = credentials.code; 169 | form.grant_type = 'authorization_code'; 170 | form.redirect_uri = this.redirect_uri; 171 | } 172 | }, 173 | // Set by credentials function 174 | set_by_credentials: () => { 175 | this.logger.debug('generateOptions: Set by credentials flow detected'); 176 | if (msg.oauth2Request) { 177 | const credentials = msg.oauth2Request.credentials || {}; 178 | form.client_id = credentials.client_id || this.client_id; 179 | form.client_secret = credentials.client_secret || this.client_secret; 180 | form.refresh_token = credentials.refresh_token || ''; 181 | } 182 | } 183 | }; 184 | 185 | // Check if the grant type from the credentials is supported and call the corresponding function 186 | if (creds.grant_type && flows[creds.grant_type]) { 187 | flows[creds.grant_type](); 188 | } 189 | // Check if the default grant type of the node is supported and call the corresponding function 190 | else if (this.grant_type && flows[this.grant_type]) { 191 | flows[this.grant_type](); 192 | } 193 | 194 | // Check if client credentials should be included in the body 195 | if (this.client_credentials_in_body) { 196 | this.logger.debug('generateOptions: Client credentials in body detected, using credentials'); 197 | form.client_id = creds.client_id || this.client_id; 198 | form.client_secret = creds.client_secret || this.client_secret; 199 | } else { 200 | // Otherwise, add the Authorization header with client credentials encoded in base64 201 | headers.Authorization = 'Basic ' + Buffer.from(`${creds.client_id || this.client_id}:${creds.client_secret || this.client_secret}`).toString('base64'); 202 | } 203 | 204 | // Set the URL to the access token URL from the message if available, otherwise use the default 205 | url = msg.oauth2Request ? msg.oauth2Request.access_token_url || this.access_token_url : this.access_token_url; 206 | 207 | // Log the final generated options 208 | this.logger.debug('generateOptions: Returning options', { method: 'POST', url, headers, form }); 209 | // Return the HTTP request options 210 | return { 211 | method: 'POST', 212 | url: url, 213 | headers: { ...headers, ...this.headers }, 214 | rejectUnauthorized: this.rejectUnauthorized, 215 | form: form 216 | }; 217 | } 218 | 219 | /** 220 | * Configures proxy settings. 221 | */ 222 | configureProxy() { 223 | // Proxy settings from environment variables or configuration 224 | const noProxyEnv = process.env.no_proxy || process.env.NO_PROXY || ''; 225 | const noProxyList = noProxyEnv.split(',').filter((entry) => entry); 226 | 227 | if (!this.proxy && !process.env.http_proxy && !process.env.HTTP_PROXY) { 228 | return; 229 | } 230 | 231 | let proxy = ''; 232 | let proxyConfig = {}; 233 | 234 | if (this.proxy) { 235 | proxyConfig = RED.nodes.getNode(this.proxy); 236 | } else { 237 | proxy = process.env.http_proxy || process.env.HTTP_PROXY; 238 | } 239 | 240 | if (proxyConfig || proxy) { 241 | const proxyURL = new URL(proxyConfig?.url || proxy); 242 | this.noproxy = proxyConfig?.noproxy || noProxyList; 243 | this.proxy = { 244 | protocol: proxyURL.protocol, 245 | hostname: proxyURL.hostname, 246 | port: proxyURL.port, 247 | username: proxyURL.username || proxyConfig?.credentials?.username || null, 248 | password: proxyURL.password || proxyConfig?.credentials?.password || null 249 | }; 250 | } 251 | 252 | this.logger.debug('configureProxy: Proxy configured', this.proxy); 253 | } 254 | 255 | /** 256 | * Checks if the URL should bypass the proxy. 257 | * @param {string} url - The URL to check. 258 | * @param {string[]} noProxyList - The list of domains to bypass the proxy. 259 | * @returns {boolean} - True if the URL should bypass the proxy, otherwise false. 260 | */ 261 | shouldBypassProxy(url, noProxyList) { 262 | const parsedUrl = new URL(url); 263 | const hostname = parsedUrl.hostname; 264 | 265 | return noProxyList.some((entry) => { 266 | if (entry === '*') { 267 | return true; 268 | } else { 269 | return hostname.endsWith(entry); 270 | } 271 | }); 272 | } 273 | 274 | /** 275 | * Cleans form data by removing undefined or empty values. 276 | * @param {Object} form - The form data. 277 | * @returns {Object} - The cleaned form data. 278 | */ 279 | cleanForm(form) { 280 | const cleanedForm = Object.fromEntries(Object.entries(form).filter(([, value]) => value !== undefined && value !== '')); 281 | this.logger.debug('cleanForm: Cleaned form data', cleanedForm); 282 | return cleanedForm; 283 | } 284 | 285 | /** 286 | * Makes a POST request. 287 | * @param {Object} options - The request options. 288 | * @returns {Promise} - The response from the request. 289 | */ 290 | async makePostRequest(options) { 291 | this.logger.debug('makePostRequest: Making POST request with options', options); 292 | 293 | const axiosOptions = { 294 | method: options.method, 295 | url: options.url, 296 | headers: options.headers, 297 | data: new URLSearchParams(options.form).toString() 298 | }; 299 | 300 | if (!options.rejectUnauthorized) { 301 | axiosOptions.httpAgent = new http.Agent({ rejectUnauthorized: options.rejectUnauthorized }); 302 | axiosOptions.httpsAgent = new https.Agent({ rejectUnauthorized: options.rejectUnauthorized }); 303 | } 304 | 305 | if (this.proxy && !this.shouldBypassProxy(options.url, this.noproxy)) { 306 | axiosOptions.proxy = { 307 | protocol: this.proxy.protocol, 308 | host: this.proxy.hostname, 309 | port: this.proxy.port, 310 | auth: { 311 | username: this.proxy.username, 312 | password: this.proxy.password 313 | } 314 | }; 315 | } else { 316 | axiosOptions.proxy = false; // Disable proxy if URL is in the NO_PROXY list 317 | } 318 | 319 | this.logger.debug('makePostRequest: Axios request options prepared', axiosOptions); 320 | 321 | return axios(axiosOptions).catch((error) => { 322 | this.logger.error('makePostRequest: Error during POST request', error); 323 | throw error; 324 | }); 325 | } 326 | 327 | /** 328 | * Handles the response from the POST request. 329 | * @param {Object} response - The response object. 330 | * @param {Object} msg - Input message object. 331 | * @param {Function} send - Function to send messages. 332 | */ 333 | handleResponse(response, msg, send) { 334 | this.logger.debug('handleResponse: Handling response', response); 335 | 336 | if (!response || !response.data) { 337 | this.logger.warn('handleResponse: Invalid response data', response); 338 | this.handleError({ message: 'Invalid response data' }, msg, send); 339 | return; 340 | } 341 | 342 | msg.oauth2Response = { ...(response.data || {}), access_token_url: this.access_token_url || '', authorization_endpoint: this.authorization_endpoint || '' }; 343 | msg.headers = response.headers || {}; // Include headers in the message 344 | this.setStatus('green', `HTTP ${response.status}, ok`); 345 | this.logger.debug('handleResponse: Response data set in message', msg); 346 | 347 | const expireTime = Math.floor(Date.now() / 1000) + (response.data.expires_in || 3600); 348 | this.credentials.access_token = response.data.access_token; 349 | this.credentials.expire_time = expireTime; 350 | this.credentials = { ...this.credentials, oauth2Response: msg.oauth2Response, headers: msg.headers }; 351 | RED.nodes.addCredentials(this.id, this.credentials); 352 | 353 | send(msg); 354 | } 355 | 356 | /** 357 | * Handles errors from the POST request. 358 | * @param {Object} error - The error object. 359 | * @param {Object} msg - Input message object. 360 | * @param {Function} send - Function to send messages. 361 | */ 362 | handleError(error, msg, send) { 363 | this.logger.error('handleError: Handling error', error); 364 | 365 | const status = error.response ? error.response.status : error.code; 366 | const message = error.response ? error.response.statusText : error.message; 367 | const data = error.response && error.response.data ? error.response.data : {}; 368 | const headers = error.response ? error.response.headers : {}; 369 | msg.oauth2Error = { status, message, data, headers }; 370 | this.setStatus('red', `HTTP ${status}, ${message}`); 371 | this.logger.debug('handleError: Error data set in message', msg); 372 | 373 | if (this.sendErrorsToCatch) { 374 | send([null, msg]); 375 | } else { 376 | this.error(msg); 377 | send([null, msg]); 378 | } 379 | } 380 | 381 | /** 382 | * Sets the status of the node. 383 | * @param {string} color - The color of the status indicator. 384 | * @param {string} text - The status text. 385 | */ 386 | setStatus(color, text) { 387 | this.logger.debug('setStatus: Setting status', { color, text }); 388 | this.status({ fill: color, shape: 'dot', text }); 389 | setTimeout(() => { 390 | this.status({}); 391 | }, 250); 392 | } 393 | } 394 | 395 | /** 396 | * Endpoint to retrieve OAuth2 credentials based on a token. 397 | * @param {Object} req - The request object. 398 | * @param {Object} res - The response object. 399 | */ 400 | RED.httpAdmin.get('/oauth2/credentials/:token', (req, res) => { 401 | try { 402 | const credentials = RED.nodes.getCredentials(req.params.token); 403 | if (credentials) { 404 | res.json({ code: credentials.code, redirect_uri: credentials.redirect_uri }); 405 | } else { 406 | res.status(404).send('oauth2.error.no-credentials'); 407 | } 408 | } catch (error) { 409 | res.status(500).send('oauth2.error.server-error'); 410 | } 411 | }); 412 | 413 | /** 414 | * Endpoint to handle OAuth2 redirect and store the authorization code. 415 | * @param {Object} req - The request object. 416 | * @param {Object} res - The response object. 417 | */ 418 | RED.httpAdmin.get('/oauth2/redirect', (req, res) => { 419 | if (req.query.code) { 420 | const [node_id] = req.query.state.split(':'); 421 | let credentials = RED.nodes.getCredentials(node_id); 422 | 423 | if (!credentials) { 424 | credentials = {}; 425 | } 426 | 427 | credentials = { ...credentials, ...req.query }; 428 | RED.nodes.addCredentials(node_id, credentials); 429 | 430 | res.send(` 431 | 432 | 433 | 442 | 443 | 444 |

Success! This page can be closed if it doesn't do so automatically.

445 | 446 | 447 | `); 448 | } else { 449 | res.status(400).send('oauth2.error.no-credentials'); 450 | } 451 | }); 452 | 453 | // Register the OAuth2Node node type 454 | RED.nodes.registerType('oauth2', OAuth2Node, { 455 | credentials: { 456 | client_id: { type: 'text' }, 457 | client_secret: { type: 'text' }, 458 | access_token: { type: 'text' }, 459 | username: { type: 'text' }, 460 | password: { type: 'text' }, 461 | refresh_token: { type: 'text' }, 462 | expire_time: { type: 'text' }, 463 | open_authentication: { type: 'text' } 464 | } 465 | }); 466 | }; 467 | -------------------------------------------------------------------------------- /test/logger_spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const sinon = require('sinon'); 3 | const Logger = require('node-red-contrib-oauth2/src/libs/logger'); // Adjust the path as necessary 4 | 5 | const loggerStatusActive = { fill: 'yellow', shape: 'ring', text: 'Logging' }; 6 | 7 | describe('Logger', function () { 8 | let consoleStub; 9 | 10 | beforeEach(function () { 11 | consoleStub = sinon.stub(console, 'log'); 12 | }); 13 | 14 | afterEach(function () { 15 | consoleStub.restore(); 16 | }); 17 | 18 | it('should initialize with default values', function () { 19 | const logger = new Logger(); 20 | assert.strictEqual(logger.label, '***'); 21 | assert.strictEqual(logger.active, true); 22 | assert.strictEqual(logger.count, null); 23 | }); 24 | 25 | it('should initialize with given values', function () { 26 | const logger = new Logger('test', false, 5); 27 | assert.strictEqual(logger.label, 'test'); 28 | assert.strictEqual(logger.active, false); 29 | assert.strictEqual(logger.count, 5); 30 | }); 31 | 32 | it('should set logger active status and count', function () { 33 | const logger = new Logger(); 34 | logger.set(false, 10); 35 | assert.strictEqual(logger.active, false); 36 | assert.strictEqual(logger.count, 10); 37 | }); 38 | 39 | it('should log a message when active', function () { 40 | const logger = new Logger(); 41 | logger.send('test message', 'info'); 42 | assert(consoleStub.calledWith(sinon.match('test message'))); 43 | }); 44 | 45 | it('should not log a message when inactive', function () { 46 | const logger = new Logger(); 47 | logger.set(false); 48 | logger.send('test message', 'info'); 49 | assert(consoleStub.notCalled); 50 | }); 51 | 52 | it('should decrement count and stop logging when count reaches zero', function () { 53 | const logger = new Logger('test', true, 1); 54 | logger.send('test message 1', 'info'); 55 | assert(consoleStub.calledOnce); 56 | logger.send('test message 2', 'info'); 57 | assert(consoleStub.calledOnce); 58 | }); 59 | 60 | it('should dump object', function () { 61 | const logger = new Logger(); 62 | const obj = { key: 'value' }; 63 | logger.objectDump(obj); 64 | assert(consoleStub.calledWith(sinon.match('key'))); 65 | assert(consoleStub.calledWith(sinon.match('value'))); 66 | }); 67 | 68 | // Additional tests to improve coverage 69 | 70 | it('should not log message when type is not specified', function () { 71 | const logger = new Logger(); 72 | logger.send('test message'); 73 | assert(consoleStub.notCalled); 74 | }); 75 | 76 | it('should log different types of messages', function () { 77 | const logger = new Logger(); 78 | logger.send('info message', 'info'); 79 | assert(consoleStub.calledWith(sinon.match('info message'))); 80 | logger.send('error message', 'error'); 81 | assert(consoleStub.calledWith(sinon.match('error message'))); 82 | }); 83 | 84 | it('should handle edge case with null count', function () { 85 | const logger = new Logger('test', true, null); 86 | logger.send('test message', 'info'); 87 | assert(consoleStub.calledWith(sinon.match('test message'))); 88 | }); 89 | 90 | it('should handle undefined or null inputs gracefully', function () { 91 | const logger = new Logger(); 92 | logger.send(null, 'info'); 93 | assert(consoleStub.notCalled); 94 | logger.send(undefined, 'info'); 95 | assert(consoleStub.notCalled); 96 | }); 97 | 98 | it('should reset count and become active when count is reset', function () { 99 | const logger = new Logger('test', true, 1); 100 | logger.send('test message 1', 'info'); 101 | assert(consoleStub.calledOnce); 102 | logger.set(true, 2); 103 | logger.send('test message 2', 'info'); 104 | assert(consoleStub.calledTwice); 105 | }); 106 | 107 | it('should not throw error on objectDump with non-object', function () { 108 | const logger = new Logger(); 109 | logger.objectDump('string'); 110 | assert(consoleStub.calledWith(sinon.match('Invalid object'))); 111 | }); 112 | 113 | // Additional tests for full coverage 114 | 115 | it('should handle sendConsole properly', function () { 116 | const logger = new Logger(); 117 | const message = 'console message'; 118 | logger.sendConsole(message, 'info'); 119 | assert(consoleStub.calledWith(sinon.match(message))); 120 | }); 121 | 122 | it('should handle sendErrorAndDump properly', function () { 123 | const logger = new Logger(); 124 | const obj = { key: 'value' }; 125 | const errorMsg = 'error message'; 126 | logger.sendErrorAndDump(errorMsg, obj); 127 | assert(consoleStub.calledWith(sinon.match(errorMsg))); 128 | assert(consoleStub.calledWith(sinon.match('key'))); 129 | }); 130 | 131 | it('should handle sendErrorAndStackDump properly', function () { 132 | const logger = new Logger(); 133 | const errorMsg = 'error message'; 134 | const error = new Error('test error'); 135 | logger.sendErrorAndStackDump(errorMsg, error); 136 | assert(consoleStub.calledWith(sinon.match(errorMsg))); 137 | assert(consoleStub.calledWith(sinon.match(error.stack))); 138 | }); 139 | 140 | it('should setNodeStatus properly', function () { 141 | const logger = new Logger(); 142 | const node = { 143 | status: sinon.spy() 144 | }; 145 | logger.setNodeStatus(node); 146 | assert(node.status.calledWith(loggerStatusActive)); 147 | }); 148 | 149 | it('should handle setOff and setOn properly', function () { 150 | const logger = new Logger('test', true, 1); 151 | logger.setOff(); 152 | assert.strictEqual(logger.active, false); 153 | logger.setOn(); 154 | assert.strictEqual(logger.active, true); 155 | assert.strictEqual(logger.count, 1); 156 | }); 157 | 158 | it('should handle stackDump properly', function () { 159 | const logger = new Logger(); 160 | const error = new Error('test error'); 161 | logger.stackDump(error); 162 | assert(consoleStub.calledWith(sinon.match(error.stack))); 163 | logger.stackDump(); 164 | assert(consoleStub.calledOnce); // As console.trace is used here 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /test/node-loading_spec.js: -------------------------------------------------------------------------------- 1 | const helper = require('node-red-node-test-helper'); 2 | const oauth2Node = require('../src/oauth2.js'); 3 | 4 | describe('OAuth2 Node Loading', function () { 5 | it('should be loaded', function (done) { 6 | this.timeout(30000); // Increase timeout for individual test 7 | console.log('Testing if node loads correctly...'); 8 | const flow = [{ id: 'n1', type: 'oauth2', name: 'oauth2' }]; 9 | helper.load(oauth2Node, flow, function () { 10 | const n1 = helper.getNode('n1'); 11 | try { 12 | n1.should.have.property('name', 'oauth2'); 13 | console.log('Node loaded successfully'); 14 | done(); 15 | } catch (err) { 16 | console.error('Node failed to load', err); 17 | done(err); 18 | } 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/oauth2/oauth2-edge-cases_spec.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); // eslint-disable-line no-unused-vars 2 | const helper = require('node-red-node-test-helper'); 3 | const nock = require('nock'); 4 | const oauth2Node = require('node-red-contrib-oauth2/src/oauth2.js'); 5 | 6 | helper.init(require.resolve('node-red')); 7 | 8 | describe('OAuth2 Node Edge Cases', function () { 9 | before(function (done) { 10 | this.timeout(20000); // Increase timeout to 20000ms for more room 11 | console.log('Starting Node-RED server...'); 12 | helper.startServer(done); 13 | }); 14 | 15 | after(function (done) { 16 | this.timeout(20000); // Increase timeout to 20000ms for more room 17 | console.log('Stopping Node-RED server...'); 18 | helper.stopServer(done); 19 | }); 20 | 21 | afterEach(function (done) { 22 | console.log('Unloading flows...'); 23 | helper.unload().then(function () { 24 | nock.cleanAll(); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should handle custom headers in response', function (done) { 30 | this.timeout(10000); // Set timeout for individual test 31 | console.log('Testing custom headers handling...'); 32 | const flow = [ 33 | { 34 | id: 'n1', 35 | type: 'oauth2', 36 | name: 'test name', 37 | wires: [['n2']], 38 | access_token_url: 'https://example.com/oauth2/token', 39 | headers: { 'X-Custom-Header': 'CustomValue' } 40 | }, 41 | { id: 'n2', type: 'helper' } 42 | ]; 43 | 44 | helper.load(oauth2Node, flow, function () { 45 | const n1 = helper.getNode('n1'); 46 | const n2 = helper.getNode('n2'); 47 | 48 | console.log('Setting up nock for example.com...'); 49 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'mocked_access_token' }, { 'x-custom-header': 'CustomValue' }); 50 | 51 | n2.on('input', function (msg) { 52 | console.log('Received input on helper node'); 53 | try { 54 | msg.should.have.property('headers'); 55 | msg.headers.should.have.property('x-custom-header', 'CustomValue'); 56 | scope.done(); 57 | done(); 58 | } catch (err) { 59 | console.error('Failed custom headers handling test', err); 60 | done(err); 61 | } 62 | }); 63 | 64 | console.log('Sending input to node...'); 65 | n1.receive({ 66 | oauth2Request: { 67 | access_token_url: 'https://example.com/oauth2/token', 68 | credentials: { 69 | grant_type: 'client_credentials', 70 | client_id: 'testClientId', 71 | client_secret: 'testClientSecret' 72 | } 73 | } 74 | }); 75 | }); 76 | }); 77 | 78 | it('should handle multiple scopes', function (done) { 79 | this.timeout(10000); // Set timeout for individual test 80 | console.log('Testing multiple scopes handling...'); 81 | const flow = [ 82 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, 83 | { id: 'n2', type: 'helper' } 84 | ]; 85 | const credentials = { 86 | clientId: 'testClientId', 87 | clientSecret: 'testClientSecret' 88 | }; 89 | 90 | helper.load(oauth2Node, flow, credentials, function () { 91 | const n1 = helper.getNode('n1'); 92 | const n2 = helper.getNode('n2'); 93 | 94 | console.log('Setting up nock for example.com...'); 95 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'mocked_access_token' }); 96 | 97 | n2.on('input', function (msg) { 98 | console.log('Received input on helper node'); 99 | try { 100 | msg.should.have.property('oauth2Response'); 101 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 102 | scope.done(); // Verify if the nock interceptor was called 103 | done(); 104 | } catch (err) { 105 | console.error('Failed multiple scopes handling test', err); 106 | done(err); 107 | } 108 | }); 109 | 110 | console.log('Sending input to node...'); 111 | n1.receive({ 112 | oauth2Request: { 113 | access_token_url: 'https://example.com/oauth2/token', 114 | credentials: { 115 | grant_type: 'client_credentials', 116 | client_id: 'testClientId', 117 | client_secret: 'testClientSecret', 118 | scope: 'scope1 scope2' 119 | } 120 | } 121 | }); 122 | }); 123 | }); 124 | 125 | it('should handle proxy settings', function (done) { 126 | this.timeout(10000); // Set timeout for individual test 127 | console.log('Testing proxy settings handling...'); 128 | const flow = [ 129 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']], access_token_url: 'https://example.com/oauth2/token' }, 130 | { id: 'n2', type: 'helper' } 131 | ]; 132 | const credentials = { 133 | clientId: 'testClientId', 134 | clientSecret: 'testClientSecret' 135 | }; 136 | const proxySettings = { 137 | prox: 'http://proxy.example.com:8080' 138 | }; 139 | 140 | helper.load(oauth2Node, flow, credentials, function () { 141 | const n1 = helper.getNode('n1'); 142 | const n2 = helper.getNode('n2'); 143 | 144 | n1.prox = proxySettings.prox; 145 | 146 | console.log('Setting up nock for example.com...'); 147 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'mocked_access_token' }); 148 | 149 | n2.on('input', function (msg) { 150 | msg.should.have.property('oauth2Response'); 151 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 152 | scope.done(); 153 | done(); 154 | }); 155 | 156 | console.log('Sending input to node...'); 157 | n1.receive({ 158 | oauth2Request: { 159 | access_token_url: 'https://example.com/oauth2/token', 160 | credentials: { 161 | grant_type: 'client_credentials', 162 | client_id: 'testClientId', 163 | client_secret: 'testClientSecret', 164 | scope: 'testScope' 165 | } 166 | } 167 | }); 168 | }); 169 | }); 170 | 171 | it('should handle network failure', function (done) { 172 | this.timeout(10000); // Set timeout for individual test 173 | console.log('Testing network failure handling...'); 174 | const flow = [ 175 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [[], ['n3']] }, 176 | { id: 'n3', type: 'helper' } 177 | ]; 178 | const credentials = { 179 | clientId: 'testClientId', 180 | clientSecret: 'testClientSecret' 181 | }; 182 | 183 | helper.load(oauth2Node, flow, credentials, function () { 184 | const n1 = helper.getNode('n1'); 185 | const n3 = helper.getNode('n3'); 186 | 187 | console.log('Setting up nock for example.com...'); 188 | const scope = nock('https://example.com').post('/oauth2/token').replyWithError('Network error'); 189 | 190 | n3.on('input', function (msg) { 191 | console.log('Received input on error helper node'); 192 | try { 193 | msg.should.have.property('oauth2Error'); 194 | msg.oauth2Error.should.have.property('message', 'Network error'); 195 | scope.done(); // Verify if the nock interceptor was called 196 | done(); 197 | } catch (err) { 198 | console.error('Failed network failure handling test', err); 199 | done(err); 200 | } 201 | }); 202 | 203 | console.log('Sending input to node...'); 204 | n1.receive({ 205 | oauth2Request: { 206 | access_token_url: 'https://example.com/oauth2/token', 207 | credentials: { 208 | grant_type: 'client_credentials', 209 | client_id: 'testClientId', 210 | client_secret: 'testClientSecret', 211 | scope: 'testScope' 212 | } 213 | } 214 | }); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /test/oauth2/oauth2-error-handling_spec.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); // eslint-disable-line no-unused-vars 2 | const helper = require('node-red-node-test-helper'); 3 | const nock = require('nock'); 4 | const oauth2Node = require('node-red-contrib-oauth2/src/oauth2.js'); 5 | 6 | helper.init(require.resolve('node-red')); 7 | 8 | describe('OAuth2 Node Error Handling', function () { 9 | before(function (done) { 10 | this.timeout(20000); // Increase timeout to 20000ms for more room 11 | console.log('Starting Node-RED server...'); 12 | helper.startServer(done); 13 | }); 14 | 15 | after(function (done) { 16 | this.timeout(20000); // Increase timeout to 20000ms for more room 17 | console.log('Stopping Node-RED server...'); 18 | helper.stopServer(done); 19 | }); 20 | 21 | afterEach(function (done) { 22 | console.log('Unloading flows...'); 23 | helper.unload().then(function () { 24 | nock.cleanAll(); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should handle errors', function (done) { 30 | this.timeout(10000); // Set timeout for individual test 31 | console.log('Testing error handling...'); 32 | const flow = [ 33 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [[], ['n3']] }, 34 | { id: 'n3', type: 'helper' } 35 | ]; 36 | const credentials = { 37 | clientId: 'testClientId', 38 | clientSecret: 'testClientSecret' 39 | }; 40 | 41 | helper.load(oauth2Node, flow, credentials, function () { 42 | const n1 = helper.getNode('n1'); 43 | const n3 = helper.getNode('n3'); 44 | 45 | console.log('Setting up nock for invalid-url.com...'); 46 | const scope = nock('https://invalid-url.com').post('/').replyWithError('mocked error'); 47 | 48 | n3.on('input', function (msg) { 49 | console.log('Received input on error helper node'); 50 | try { 51 | msg.should.have.property('oauth2Error'); 52 | msg.oauth2Error.should.have.property('message', 'mocked error'); 53 | scope.done(); // Verify if the nock interceptor was called 54 | done(); 55 | } catch (err) { 56 | console.error('Failed error handling test', err); 57 | done(err); 58 | } 59 | }); 60 | 61 | console.log('Sending input to node...'); 62 | n1.receive({ 63 | oauth2Request: { 64 | access_token_url: 'https://invalid-url.com', 65 | credentials: { 66 | grant_type: 'client_credentials', 67 | client_id: 'testClientId', 68 | client_secret: 'testClientSecret', 69 | scope: 'testScope' 70 | } 71 | } 72 | }); 73 | }); 74 | }); 75 | 76 | it('should handle invalid client credentials', function (done) { 77 | this.timeout(10000); // Set timeout for individual test 78 | console.log('Testing invalid client credentials handling...'); 79 | const flow = [ 80 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [[], ['n3']] }, 81 | { id: 'n3', type: 'helper' } 82 | ]; 83 | const credentials = { 84 | clientId: 'invalidClientId', 85 | clientSecret: 'invalidClientSecret' 86 | }; 87 | 88 | helper.load(oauth2Node, flow, credentials, function () { 89 | const n1 = helper.getNode('n1'); 90 | const n3 = helper.getNode('n3'); 91 | 92 | console.log('Setting up nock for example.com...'); 93 | const scope = nock('https://example.com').post('/oauth2/token').reply(401, { error: 'invalid_client' }); 94 | 95 | n3.on('input', function (msg) { 96 | console.log('Received input on error helper node'); 97 | try { 98 | msg.should.have.property('oauth2Error'); 99 | msg.oauth2Error.should.have.property('status', 401); 100 | msg.oauth2Error.data.should.have.property('error', 'invalid_client'); 101 | scope.done(); // Verify if the nock interceptor was called 102 | done(); 103 | } catch (err) { 104 | console.error('Failed invalid client credentials handling test', err); 105 | done(err); 106 | } 107 | }); 108 | 109 | console.log('Sending input to node...'); 110 | n1.receive({ 111 | oauth2Request: { 112 | access_token_url: 'https://example.com/oauth2/token', 113 | credentials: { 114 | grant_type: 'client_credentials', 115 | client_id: 'invalidClientId', 116 | client_secret: 'invalidClientSecret', 117 | scope: 'testScope' 118 | } 119 | } 120 | }); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/oauth2/oauth2-flows_spec.js: -------------------------------------------------------------------------------- 1 | const helper = require('node-red-node-test-helper'); 2 | const nock = require('nock'); 3 | const oauth2Node = require('node-red-contrib-oauth2/src/oauth2.js'); 4 | 5 | describe('OAuth2 Node Flows', function () { 6 | it('should handle input and make POST request', async function () { 7 | this.timeout(30000); // Increase timeout for individual test 8 | console.log('Testing input handling and POST request...'); 9 | const flow = [ 10 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, 11 | { id: 'n2', type: 'helper' } 12 | ]; 13 | const credentials = { 14 | clientId: 'testClientId', 15 | clientSecret: 'testClientSecret' 16 | }; 17 | 18 | await helper.load(oauth2Node, flow, credentials); 19 | const n1 = helper.getNode('n1'); 20 | const n2 = helper.getNode('n2'); 21 | 22 | console.log('Setting up nock for example.com...'); 23 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'mocked_access_token' }); 24 | 25 | return new Promise((resolve, reject) => { 26 | n2.on('input', function (msg) { 27 | console.log('Received input on helper node'); 28 | try { 29 | msg.should.have.property('oauth2Response'); 30 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 31 | scope.done(); // Verify if the nock interceptor was called 32 | resolve(); 33 | } catch (err) { 34 | console.error('Failed input handling test', err); 35 | reject(err); 36 | } 37 | }); 38 | 39 | console.log('Sending input to node...'); 40 | n1.receive({ 41 | oauth2Request: { 42 | access_token_url: 'https://example.com/oauth2/token', 43 | credentials: { 44 | grant_type: 'client_credentials', 45 | client_id: 'testClientId', 46 | client_secret: 'testClientSecret', 47 | scope: 'testScope' 48 | } 49 | } 50 | }); 51 | }); 52 | }); 53 | 54 | it('should handle authorization_code grant type', function (done) { 55 | this.timeout(10000); // Set timeout for individual test 56 | console.log('Testing authorization_code grant type handling...'); 57 | const flow = [ 58 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, 59 | { id: 'n2', type: 'helper' } 60 | ]; 61 | const credentials = { 62 | clientId: 'testClientId', 63 | clientSecret: 'testClientSecret' 64 | }; 65 | 66 | helper.load(oauth2Node, flow, credentials, function () { 67 | const n1 = helper.getNode('n1'); 68 | const n2 = helper.getNode('n2'); 69 | 70 | console.log('Setting up nock for example.com...'); 71 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { 72 | access_token: 'mocked_access_token', 73 | refresh_token: 'mocked_refresh_token' 74 | }); 75 | 76 | n2.on('input', function (msg) { 77 | console.log('Received input on helper node'); 78 | try { 79 | msg.should.have.property('oauth2Response'); 80 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 81 | msg.oauth2Response.should.have.property('refresh_token', 'mocked_refresh_token'); 82 | scope.done(); // Verify if the nock interceptor was called 83 | done(); 84 | } catch (err) { 85 | console.error('Failed authorization_code grant type handling test', err); 86 | done(err); 87 | } 88 | }); 89 | 90 | console.log('Sending input to node...'); 91 | n1.receive({ 92 | oauth2Request: { 93 | access_token_url: 'https://example.com/oauth2/token', 94 | credentials: { 95 | grant_type: 'authorization_code', 96 | code: 'testAuthorizationCode', 97 | redirect_uri: 'https://example.com/redirect', 98 | client_id: 'testClientId', 99 | client_secret: 'testClientSecret' 100 | } 101 | } 102 | }); 103 | }); 104 | }); 105 | 106 | // Add other OAuth2 flow related tests here... 107 | }); 108 | -------------------------------------------------------------------------------- /test/oauth2/oauth2-generate-options_spec.js: -------------------------------------------------------------------------------- 1 | const helper = require('node-red-node-test-helper'); 2 | const OAuth2Node = require('node-red-contrib-oauth2/src/oauth2.js'); 3 | 4 | helper.init(require.resolve('node-red')); 5 | 6 | describe('OAuth2 Node generateOptions', function () { 7 | const defaultCredentials = { 8 | client_id: 'testClientId', 9 | client_secret: 'testClientSecret', 10 | username: 'testUser', 11 | password: 'testPassword', 12 | refresh_token: 'testRefreshToken', 13 | redirect_uri: 'https://example.com/redirect', 14 | scope: 'testScope', 15 | resource: 'testResource', 16 | state: 'testState' 17 | }; 18 | 19 | const createFlow = (credentials = defaultCredentials) => [ 20 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']], ...credentials }, 21 | { id: 'n2', type: 'helper' } 22 | ]; 23 | 24 | const testGenerateOptions = (flow, input, expectedForm, expectedHeaders, done) => { 25 | helper.load(OAuth2Node, flow, function () { 26 | const n1 = helper.getNode('n1'); 27 | console.log('Input to generateOptions:', JSON.stringify(input, null, 2)); 28 | const options = n1.generateOptions(input); 29 | console.log('Generated options:', JSON.stringify(options, null, 2)); 30 | try { 31 | options.form.should.deepEqual(expectedForm); 32 | options.headers.should.deepEqual(expectedHeaders); 33 | done(); 34 | } catch (err) { 35 | done(err); 36 | } 37 | }); 38 | }; 39 | 40 | this.timeout(60000); // Increase timeout to 60 seconds 41 | 42 | it('should handle password flow', function (done) { 43 | const flow = createFlow(); 44 | const input = { 45 | oauth2Request: { 46 | credentials: { 47 | grant_type: 'password', 48 | client_id: 'testClientId', 49 | client_secret: 'testClientSecret', 50 | username: 'testUser', 51 | password: 'testPassword', 52 | scope: 'testScope', 53 | resource: 'testResource', 54 | state: 'testState' 55 | } 56 | } 57 | }; 58 | 59 | const expectedForm = { 60 | grant_type: 'password', 61 | username: 'testUser', 62 | password: 'testPassword', 63 | scope: 'testScope', 64 | resource: 'testResource', 65 | state: 'testState' 66 | }; 67 | 68 | const expectedHeaders = { 69 | 'Content-Type': 'application/x-www-form-urlencoded', 70 | Accept: 'application/json', 71 | Authorization: 'Basic ' + Buffer.from('testClientId:testClientSecret').toString('base64') 72 | }; 73 | 74 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 75 | }); 76 | 77 | it('should handle client credentials flow', function (done) { 78 | const flow = createFlow(); 79 | const input = { 80 | oauth2Request: { 81 | credentials: { 82 | grant_type: 'client_credentials', 83 | client_id: 'testClientId', 84 | client_secret: 'testClientSecret', 85 | scope: 'testScope', 86 | resource: 'testResource', 87 | state: 'testState' 88 | } 89 | } 90 | }; 91 | 92 | const expectedForm = { 93 | grant_type: 'client_credentials', 94 | scope: 'testScope', 95 | resource: 'testResource', 96 | state: 'testState' 97 | }; 98 | 99 | const expectedHeaders = { 100 | 'Content-Type': 'application/x-www-form-urlencoded', 101 | Accept: 'application/json', 102 | Authorization: 'Basic ' + Buffer.from('testClientId:testClientSecret').toString('base64') 103 | }; 104 | 105 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 106 | }); 107 | 108 | it('should handle refresh token flow', function (done) { 109 | const flow = createFlow(); 110 | const input = { 111 | oauth2Request: { 112 | credentials: { 113 | grant_type: 'refresh_token', 114 | client_id: 'testClientId', 115 | client_secret: 'testClientSecret', 116 | refresh_token: 'testRefreshToken', 117 | scope: 'testScope', 118 | resource: 'testResource', 119 | state: 'testState' 120 | } 121 | } 122 | }; 123 | 124 | const expectedForm = { 125 | grant_type: 'refresh_token', 126 | client_id: 'testClientId', 127 | client_secret: 'testClientSecret', 128 | refresh_token: 'testRefreshToken', 129 | scope: 'testScope', 130 | resource: 'testResource', 131 | state: 'testState' 132 | }; 133 | 134 | const expectedHeaders = { 135 | 'Content-Type': 'application/x-www-form-urlencoded', 136 | Accept: 'application/json', 137 | Authorization: 'Basic ' + Buffer.from('testClientId:testClientSecret').toString('base64') 138 | }; 139 | 140 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 141 | }); 142 | 143 | it('should handle authorization code flow', function (done) { 144 | const flow = createFlow(); 145 | const input = { 146 | oauth2Request: { 147 | credentials: { 148 | grant_type: 'authorization_code', 149 | client_id: 'testClientId', 150 | client_secret: 'testClientSecret', 151 | code: 'testCode', 152 | redirect_uri: 'https://example.com/redirect', 153 | scope: 'testScope', 154 | resource: 'testResource', 155 | state: 'testState' 156 | } 157 | } 158 | }; 159 | 160 | const expectedForm = { 161 | grant_type: 'authorization_code', 162 | code: undefined, 163 | redirect_uri: 'https://example.com/redirect', 164 | scope: 'testScope', 165 | resource: 'testResource', 166 | state: 'testState' 167 | }; 168 | 169 | const expectedHeaders = { 170 | 'Content-Type': 'application/x-www-form-urlencoded', 171 | Accept: 'application/json', 172 | Authorization: 'Basic ' + Buffer.from('testClientId:testClientSecret').toString('base64') 173 | }; 174 | 175 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 176 | }); 177 | 178 | it('should handle implicit flow', function (done) { 179 | const flow = createFlow(); 180 | const input = { 181 | oauth2Request: { 182 | credentials: { 183 | grant_type: 'implicit_flow', 184 | client_id: 'testClientId', 185 | client_secret: 'testClientSecret', 186 | code: 'testCode', 187 | redirect_uri: 'https://example.com/redirect', 188 | scope: 'testScope', 189 | resource: 'testResource', 190 | state: 'testState' 191 | } 192 | } 193 | }; 194 | 195 | const expectedForm = { 196 | grant_type: 'authorization_code', 197 | client_id: 'testClientId', 198 | client_secret: 'testClientSecret', 199 | code: undefined, 200 | redirect_uri: 'https://example.com/redirect', 201 | scope: 'testScope', 202 | resource: 'testResource', 203 | state: 'testState' 204 | }; 205 | 206 | const expectedHeaders = { 207 | 'Content-Type': 'application/x-www-form-urlencoded', 208 | Accept: 'application/json', 209 | Authorization: 'Basic ' + Buffer.from('testClientId:testClientSecret').toString('base64') 210 | }; 211 | 212 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 213 | }); 214 | 215 | it('should handle set by credentials flow', function (done) { 216 | const flow = createFlow(); 217 | const input = { 218 | oauth2Request: { 219 | credentials: { 220 | grant_type: 'set_by_credentials', 221 | client_id: 'setClientId', 222 | client_secret: 'setClientSecret', 223 | refresh_token: 'setRefreshToken', 224 | scope: 'testScope', 225 | resource: 'testResource', 226 | state: 'testState' 227 | } 228 | } 229 | }; 230 | 231 | const expectedForm = { 232 | grant_type: 'set_by_credentials', 233 | client_id: 'setClientId', 234 | client_secret: 'setClientSecret', 235 | refresh_token: 'setRefreshToken', 236 | scope: 'testScope', 237 | resource: 'testResource', 238 | state: 'testState' 239 | }; 240 | const expectedHeaders = { 241 | Accept: 'application/json', 242 | Authorization: 'Basic ' + Buffer.from('setClientId:setClientSecret').toString('base64'), 243 | 'Content-Type': 'application/x-www-form-urlencoded' 244 | }; 245 | 246 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 247 | }); 248 | 249 | it('should include client credentials in the body if configured', function (done) { 250 | const flow = createFlow({ client_credentials_in_body: true }); 251 | const input = { 252 | oauth2Request: { 253 | credentials: { 254 | grant_type: 'client_credentials', 255 | client_id: 'testClientId', 256 | client_secret: 'testClientSecret', 257 | scope: 'testScope', 258 | resource: 'testResource', 259 | state: 'testState' 260 | } 261 | } 262 | }; 263 | 264 | const expectedForm = { 265 | grant_type: 'client_credentials', 266 | client_id: 'testClientId', 267 | client_secret: 'testClientSecret', 268 | scope: 'testScope', 269 | resource: 'testResource', 270 | state: 'testState' 271 | }; 272 | 273 | const expectedHeaders = { 274 | 'Content-Type': 'application/x-www-form-urlencoded', 275 | Accept: 'application/json' 276 | }; 277 | 278 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 279 | }); 280 | 281 | it('should add Authorization header with client credentials if not in body', function (done) { 282 | const flow = createFlow({ client_credentials_in_body: false }); 283 | const input = { 284 | oauth2Request: { 285 | credentials: { 286 | grant_type: 'client_credentials', 287 | client_id: 'testClientId', 288 | client_secret: 'testClientSecret', 289 | scope: 'testScope', 290 | resource: 'testResource', 291 | state: 'testState' 292 | } 293 | } 294 | }; 295 | 296 | const expectedForm = { 297 | grant_type: 'client_credentials', 298 | scope: 'testScope', 299 | resource: 'testResource', 300 | state: 'testState' 301 | }; 302 | 303 | const expectedHeaders = { 304 | 'Content-Type': 'application/x-www-form-urlencoded', 305 | Accept: 'application/json', 306 | Authorization: 'Basic ' + Buffer.from('testClientId:testClientSecret').toString('base64') 307 | }; 308 | 309 | testGenerateOptions(flow, input, expectedForm, expectedHeaders, done); 310 | }); 311 | 312 | it('should use access token URL from the message if available', function (done) { 313 | const flow = createFlow(); 314 | const input = { 315 | oauth2Request: { 316 | access_token_url: 'https://custom.example.com/token', 317 | credentials: { 318 | grant_type: 'client_credentials', 319 | client_id: 'testClientId', 320 | client_secret: 'testClientSecret', 321 | scope: 'testScope', 322 | resource: 'testResource', 323 | state: 'testState' 324 | } 325 | } 326 | }; 327 | 328 | const expectedForm = { 329 | grant_type: 'client_credentials', 330 | scope: 'testScope', 331 | resource: 'testResource', 332 | state: 'testState' 333 | }; 334 | 335 | const expectedHeaders = { 336 | 'Content-Type': 'application/x-www-form-urlencoded', 337 | Accept: 'application/json', 338 | Authorization: 'Basic ' + Buffer.from('testClientId:testClientSecret').toString('base64') 339 | }; 340 | 341 | helper.load(OAuth2Node, flow, function () { 342 | const n1 = helper.getNode('n1'); 343 | console.log('Input to generateOptions:', JSON.stringify(input, null, 2)); 344 | const options = n1.generateOptions(input); 345 | console.log('Generated options:', JSON.stringify(options, null, 2)); 346 | try { 347 | options.form.should.deepEqual(expectedForm); 348 | options.headers.should.deepEqual(expectedHeaders); 349 | options.url.should.equal('https://custom.example.com/token'); 350 | done(); 351 | } catch (err) { 352 | done(err); 353 | } 354 | }); 355 | }); 356 | }); 357 | -------------------------------------------------------------------------------- /test/oauth2/oauth2-grant-types_spec.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); // eslint-disable-line no-unused-vars 2 | const helper = require('node-red-node-test-helper'); 3 | const nock = require('nock'); 4 | const OAuth2Node = require('node-red-contrib-oauth2/src/oauth2.js'); // Adjust the path as needed 5 | 6 | helper.init(require.resolve('node-red')); 7 | 8 | describe('OAuth2 Node Grant Types', function () { 9 | before(function (done) { 10 | this.timeout(20000); // Increase timeout to 20000ms for more room 11 | console.log('Starting Node-RED server...'); 12 | helper.startServer(done); 13 | }); 14 | 15 | after(function (done) { 16 | this.timeout(20000); // Increase timeout to 20000ms for more room 17 | console.log('Stopping Node-RED server...'); 18 | helper.stopServer(done); 19 | }); 20 | 21 | afterEach(function (done) { 22 | console.log('Unloading flows...'); 23 | helper.unload().then(function () { 24 | nock.cleanAll(); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should handle authorization_code grant type', function (done) { 30 | this.timeout(10000); // Set timeout for individual test 31 | console.log('Testing authorization_code grant type handling...'); 32 | const flow = [ 33 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, 34 | { id: 'n2', type: 'helper' } 35 | ]; 36 | const credentials = { 37 | clientId: 'testClientId', 38 | clientSecret: 'testClientSecret' 39 | }; 40 | 41 | helper.load(OAuth2Node, flow, credentials, function () { 42 | const n1 = helper.getNode('n1'); 43 | const n2 = helper.getNode('n2'); 44 | 45 | console.log('Setting up nock for example.com...'); 46 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { 47 | access_token: 'mocked_access_token', 48 | refresh_token: 'mocked_refresh_token' 49 | }); 50 | 51 | n2.on('input', function (msg) { 52 | console.log('Received input on helper node'); 53 | try { 54 | msg.should.have.property('oauth2Response'); 55 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 56 | msg.oauth2Response.should.have.property('refresh_token', 'mocked_refresh_token'); 57 | scope.done(); // Verify if the nock interceptor was called 58 | done(); 59 | } catch (err) { 60 | console.error('Failed authorization_code grant type handling test', err); 61 | done(err); 62 | } 63 | }); 64 | 65 | console.log('Sending input to node...'); 66 | n1.receive({ 67 | oauth2Request: { 68 | access_token_url: 'https://example.com/oauth2/token', 69 | credentials: { 70 | grant_type: 'authorization_code', 71 | code: 'testAuthorizationCode', 72 | redirect_uri: 'https://example.com/redirect', 73 | client_id: 'testClientId', 74 | client_secret: 'testClientSecret' 75 | } 76 | } 77 | }); 78 | }); 79 | }); 80 | 81 | it('should handle refresh_token grant type', function (done) { 82 | this.timeout(10000); // Set timeout for individual test 83 | console.log('Testing refresh_token grant type handling...'); 84 | const flow = [ 85 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, 86 | { id: 'n2', type: 'helper' } 87 | ]; 88 | const credentials = { 89 | clientId: 'testClientId', 90 | clientSecret: 'testClientSecret' 91 | }; 92 | 93 | helper.load(OAuth2Node, flow, credentials, function () { 94 | const n1 = helper.getNode('n1'); 95 | const n2 = helper.getNode('n2'); 96 | 97 | console.log('Setting up nock for example.com...'); 98 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'new_mocked_access_token' }); 99 | 100 | n2.on('input', function (msg) { 101 | console.log('Received input on helper node'); 102 | try { 103 | msg.should.have.property('oauth2Response'); 104 | msg.oauth2Response.should.have.property('access_token', 'new_mocked_access_token'); 105 | scope.done(); // Verify if the nock interceptor was called 106 | done(); 107 | } catch (err) { 108 | console.error('Failed refresh_token grant type handling test', err); 109 | done(err); 110 | } 111 | }); 112 | 113 | console.log('Sending input to node...'); 114 | n1.receive({ 115 | oauth2Request: { 116 | access_token_url: 'https://example.com/oauth2/token', 117 | credentials: { 118 | grant_type: 'refresh_token', 119 | refresh_token: 'testRefreshToken', 120 | client_id: 'testClientId', 121 | client_secret: 'testClientSecret' 122 | } 123 | } 124 | }); 125 | }); 126 | }); 127 | 128 | it('should handle password grant type', function (done) { 129 | this.timeout(10000); // Set timeout for individual test 130 | console.log('Testing password grant type handling...'); 131 | const flow = [ 132 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, 133 | { id: 'n2', type: 'helper' } 134 | ]; 135 | const credentials = { 136 | clientId: 'testClientId', 137 | clientSecret: 'testClientSecret', 138 | username: 'testUser', 139 | password: 'testPassword' 140 | }; 141 | 142 | helper.load(OAuth2Node, flow, credentials, function () { 143 | const n1 = helper.getNode('n1'); 144 | const n2 = helper.getNode('n2'); 145 | 146 | console.log('Setting up nock for example.com...'); 147 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'mocked_access_token' }); 148 | 149 | n2.on('input', function (msg) { 150 | console.log('Received input on helper node'); 151 | try { 152 | msg.should.have.property('oauth2Response'); 153 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 154 | scope.done(); // Verify if the nock interceptor was called 155 | done(); 156 | } catch (err) { 157 | console.error('Failed password grant type handling test', err); 158 | done(err); 159 | } 160 | }); 161 | 162 | console.log('Sending input to node...'); 163 | n1.receive({ 164 | oauth2Request: { 165 | access_token_url: 'https://example.com/oauth2/token', 166 | credentials: { 167 | grant_type: 'password', 168 | username: 'testUser', 169 | password: 'testPassword', 170 | client_id: 'testClientId', 171 | client_secret: 'testClientSecret', 172 | scope: 'testScope' 173 | } 174 | } 175 | }); 176 | }); 177 | }); 178 | 179 | it('should handle default grant_type', async function () { 180 | this.timeout(10000); // Set timeout for individual test 181 | console.log('Starting test case...'); 182 | 183 | const flow = [ 184 | { id: 'n1', type: 'oauth2', name: 'oauth2', grant_type: 'client_credentials', wires: [['n2']] }, 185 | { id: 'n2', type: 'helper' } 186 | ]; 187 | const credentials = { 188 | clientId: 'defaultClientId', 189 | clientSecret: 'defaultClientSecret' 190 | }; 191 | 192 | await helper.load(OAuth2Node, flow, credentials); 193 | console.log('Flow loaded...'); 194 | 195 | const n1 = helper.getNode('n1'); 196 | const n2 = helper.getNode('n2'); 197 | 198 | nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'default_access_token' }); 199 | 200 | return new Promise((resolve, reject) => { 201 | n2.on('input', function (msg) { 202 | console.log('Received input message...'); 203 | try { 204 | msg.should.have.property('oauth2Response'); 205 | msg.oauth2Response.should.have.property('access_token', 'default_access_token'); 206 | // Signal the completion of the test 207 | console.log('Test case completed successfully.'); 208 | resolve(); 209 | } catch (err) { 210 | console.error('Error during test execution:', err); 211 | reject(err); 212 | } 213 | }); 214 | 215 | console.log('Sending message to n1...'); 216 | n1.receive({ 217 | oauth2Request: { 218 | access_token_url: 'https://example.com/oauth2/token', 219 | credentials: { 220 | grant_type: 'client_credentials', 221 | client_id: 'defaultClientId', 222 | client_secret: 'defaultClientSecret' 223 | } 224 | } 225 | }); 226 | }); 227 | }); 228 | 229 | it('should handle password grant type in generateOptions', function (done) { 230 | console.log('Testing password grant type handling in generateOptions...'); 231 | const flow = [ 232 | { id: 'n1', type: 'oauth2', name: 'oauth2', grant_type: 'password', username: 'testUser', password: 'testPassword', wires: [['n2']] }, 233 | { id: 'n2', type: 'helper' } 234 | ]; 235 | const credentials = { 236 | grant_type: 'password', 237 | clientId: 'testClientId', 238 | clientSecret: 'testClientSecret' 239 | }; 240 | 241 | helper.load(OAuth2Node, flow, credentials, function () { 242 | const n1 = helper.getNode('n1'); 243 | const options = n1.generateOptions({ oauth2Request: { credentials } }); 244 | 245 | try { 246 | options.form.should.have.property('username', 'testUser'); 247 | options.form.should.have.property('password', 'testPassword'); 248 | done(); 249 | } catch (err) { 250 | done(err); 251 | } 252 | }); 253 | }); 254 | it('should cover client credentials in body', function (done) { 255 | this.timeout(10000); // Set timeout for individual test 256 | console.log('Testing client credentials in body...'); 257 | const flow = [ 258 | { id: 'n1', type: 'oauth2', name: 'oauth2', client_credentials_in_body: true, wires: [['n2']] }, 259 | { id: 'n2', type: 'helper' } 260 | ]; 261 | const credentials = { 262 | clientId: 'testClientId', 263 | clientSecret: 'testClientSecret' 264 | }; 265 | 266 | helper.load(OAuth2Node, flow, credentials, function () { 267 | const n1 = helper.getNode('n1'); 268 | const n2 = helper.getNode('n2'); 269 | 270 | console.log('Setting up nock for example.com...'); 271 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'mocked_access_token' }); 272 | 273 | n2.on('input', function (msg) { 274 | console.log('Received input on helper node'); 275 | try { 276 | msg.should.have.property('oauth2Response'); 277 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 278 | scope.done(); // Verify if the nock interceptor was called 279 | done(); 280 | } catch (err) { 281 | console.error('Failed client credentials in body test', err); 282 | done(err); 283 | } 284 | }); 285 | 286 | console.log('Sending input to node...'); 287 | n1.receive({ 288 | oauth2Request: { 289 | access_token_url: 'https://example.com/oauth2/token', 290 | credentials: { 291 | grant_type: 'client_credentials', 292 | client_id: 'testClientId', 293 | client_secret: 'testClientSecret' 294 | } 295 | } 296 | }); 297 | }); 298 | }); 299 | }); 300 | -------------------------------------------------------------------------------- /test/oauth2/oauth2-loading_spec.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); // eslint-disable-line no-unused-vars 2 | const helper = require('node-red-node-test-helper'); 3 | const oauth2Node = require('node-red-contrib-oauth2/src/oauth2.js'); 4 | const nock = require('nock'); 5 | 6 | helper.init(require.resolve('node-red')); 7 | 8 | describe('OAuth2 Node Loading', function () { 9 | before(function (done) { 10 | this.timeout(20000); // Increase timeout to 20000ms for more room 11 | console.log('Starting Node-RED server...'); 12 | helper.startServer(done); 13 | }); 14 | 15 | after(function (done) { 16 | this.timeout(20000); // Increase timeout to 20000ms for more room 17 | console.log('Stopping Node-RED server...'); 18 | helper.stopServer(done); 19 | }); 20 | 21 | afterEach(function (done) { 22 | console.log('Unloading flows...'); 23 | helper.unload().then(function () { 24 | nock.cleanAll(); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should be loaded', function (done) { 30 | this.timeout(30000); // Increase timeout for individual test 31 | console.log('Testing if node loads correctly...'); 32 | const flow = [{ id: 'n1', type: 'oauth2', name: 'oauth2' }]; 33 | helper.load(oauth2Node, flow, function () { 34 | const n1 = helper.getNode('n1'); 35 | try { 36 | n1.should.have.property('name', 'oauth2'); 37 | console.log('Node loaded successfully'); 38 | done(); 39 | } catch (err) { 40 | console.error('Node failed to load', err); 41 | done(err); 42 | } 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/oauth2/oauth2-post-request_spec.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); // eslint-disable-line no-unused-vars 2 | const helper = require('node-red-node-test-helper'); 3 | const nock = require('nock'); 4 | const oauth2Node = require('node-red-contrib-oauth2/src/oauth2.js'); 5 | 6 | helper.init(require.resolve('node-red')); 7 | 8 | describe('OAuth2 Node POST Request', function () { 9 | before(function (done) { 10 | this.timeout(20000); // Increase timeout to 20000ms for more room 11 | console.log('Starting Node-RED server...'); 12 | helper.startServer(done); 13 | }); 14 | 15 | after(function (done) { 16 | this.timeout(20000); // Increase timeout to 20000ms for more room 17 | console.log('Stopping Node-RED server...'); 18 | helper.stopServer(done); 19 | }); 20 | 21 | afterEach(function (done) { 22 | console.log('Unloading flows...'); 23 | helper.unload().then(function () { 24 | nock.cleanAll(); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should handle input and make POST request', async function () { 30 | this.timeout(30000); // Increase timeout for individual test 31 | console.log('Testing input handling and POST request...'); 32 | const flow = [ 33 | { id: 'n1', type: 'oauth2', name: 'oauth2', wires: [['n2']] }, 34 | { id: 'n2', type: 'helper' } 35 | ]; 36 | const credentials = { 37 | clientId: 'testClientId', 38 | clientSecret: 'testClientSecret' 39 | }; 40 | 41 | await helper.load(oauth2Node, flow, credentials); 42 | const n1 = helper.getNode('n1'); 43 | const n2 = helper.getNode('n2'); 44 | 45 | console.log('Setting up nock for example.com...'); 46 | const scope = nock('https://example.com').post('/oauth2/token').reply(200, { access_token: 'mocked_access_token' }); 47 | 48 | return new Promise((resolve, reject) => { 49 | n2.on('input', function (msg) { 50 | console.log('Received input on helper node'); 51 | try { 52 | msg.should.have.property('oauth2Response'); 53 | msg.oauth2Response.should.have.property('access_token', 'mocked_access_token'); 54 | scope.done(); // Verify if the nock interceptor was called 55 | resolve(); 56 | } catch (err) { 57 | console.error('Failed input handling test', err); 58 | reject(err); 59 | } 60 | }); 61 | 62 | console.log('Sending input to node...'); 63 | n1.receive({ 64 | oauth2Request: { 65 | access_token_url: 'https://example.com/oauth2/token', 66 | credentials: { 67 | grant_type: 'client_credentials', 68 | client_id: 'testClientId', 69 | client_secret: 'testClientSecret', 70 | scope: 'testScope' 71 | } 72 | } 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/setup-teardown._spec.js: -------------------------------------------------------------------------------- 1 | const helper = require('node-red-node-test-helper'); 2 | const nock = require('nock'); 3 | 4 | helper.init(require.resolve('node-red')); 5 | 6 | let serverStarted = false; 7 | 8 | function startServer() { 9 | return new Promise((resolve, reject) => { 10 | console.log('Starting Node-RED server...'); 11 | helper.startServer((err) => { 12 | if (err) { 13 | console.error('Failed to start Node-RED server:', err); 14 | return reject(err); 15 | } 16 | serverStarted = true; 17 | console.log('Node-RED server started successfully.'); 18 | resolve(); 19 | }); 20 | }); 21 | } 22 | 23 | function stopServer() { 24 | return new Promise((resolve, reject) => { 25 | console.log('Stopping Node-RED server...'); 26 | if (serverStarted && helper._server && helper._server.listening) { 27 | console.log('Node-RED server is running, attempting to stop it...'); 28 | helper.stopServer((err) => { 29 | if (err) { 30 | console.error('Error stopping Node-RED server:', err); 31 | return reject(err); 32 | } 33 | console.log('Node-RED server stopped successfully.'); 34 | serverStarted = false; 35 | resolve(); 36 | }); 37 | } else { 38 | console.log('Node-RED server is not running or was never started.'); 39 | resolve(); 40 | } 41 | }); 42 | } 43 | 44 | before(async function () { 45 | this.timeout(20000); // Increase timeout to 20000ms for more room 46 | await startServer(); 47 | }); 48 | 49 | after(async function () { 50 | this.timeout(20000); // Increase timeout to 20000ms for more room 51 | await stopServer().catch((err) => { 52 | console.error('Failed to stop Node-RED server:', err); 53 | }); 54 | }); 55 | 56 | afterEach(function (done) { 57 | console.log('Unloading flows...'); 58 | helper 59 | .unload() 60 | .then(() => { 61 | console.log('Flows unloaded successfully.'); 62 | nock.cleanAll(); 63 | done(); 64 | }) 65 | .catch((err) => { 66 | console.error('Error unloading flows:', err); 67 | done(err); 68 | }); 69 | }); 70 | 71 | // Exit process after all tests are done to ensure coverage data is written 72 | after(function () { 73 | stopServer() 74 | .then(() => { 75 | process.exit(); 76 | }) 77 | .catch((err) => { 78 | console.error('Failed to stop Node-RED server:', err); 79 | process.exit(1); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/utils/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUNaiIC8Bg0d1Far4JvSBs3xAFSn0wDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA1MjUwMDU3NDdaFw0yNTA1 5 | MjUwMDU3NDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQC1/Kh2f+xsLxKkX0Iemw+whCLupVNSXeUpYi9PCzoV 8 | FIJs8PSWDHyT+EA+qRwjiY3GQFhhJIZmhLDXuJb57h8nYS4+M8PRbvU1LZkC2daY 9 | 9sFAppr0ppKZAQnSNytX7YKFTbai5qoF5NmIYYB2gOFg6/3UGyjNEBVG5O4oYo1K 10 | Sh+/xYBeGNafFSlyu/16XtNMT7VrlzeRmDeabzFI8lLIte2oI9fO0XyB9ptNq9pS 11 | OFfbZV9z+ecqQDleYUFCXYbrCLwimtUbHxqOlafyioj6zwFvjWges1flc3iIvA9C 12 | WDLwbnJ5nNhou8oGfpB09Ydm5heDDGiTTUjUQS5U+4FDAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBQYIibLUPZEOwvf22B0YYdeZfBoAzAfBgNVHSMEGDAWgBQYIibLUPZEOwvf 14 | 22B0YYdeZfBoAzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCE 15 | 25z28j2fisnc/i0ndq6nx5lBqDNKxmMr27esd3shAbJQkVjGEtA9SoBMfDMrlpiL 16 | 7asoU/+Y5g9+FkajML4sBVkCXQ7Py0sWy0iFG4xHuRRi6LpZOIc+XhcSJ5MYTW05 17 | mc81EEMVnay/xGt3wXpVO8MMiLs0Dy8s6R7K8XdqUa9zugYsN6iMwEcOcC06/GFP 18 | mRevO+GGo4MMud6IH6JxClKCizDyLHYVGeL/2OHN3AA7o+MIHiolIgr4l6AJ/dP6 19 | mXyyoN1GaLe1BZmrptQhCSJqsCik74aYRHN5IG8buGv9Kd0MIG5wIgyQGtnHEGmX 20 | M+pVKdDuS8LLqkF3hyGz 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/utils/private-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAtfyodn/sbC8SpF9CHpsPsIQi7qVTUl3lKWIvTws6FRSCbPD0 3 | lgx8k/hAPqkcI4mNxkBYYSSGZoSw17iW+e4fJ2EuPjPD0W71NS2ZAtnWmPbBQKaa 4 | 9KaSmQEJ0jcrV+2ChU22ouaqBeTZiGGAdoDhYOv91BsozRAVRuTuKGKNSkofv8WA 5 | XhjWnxUpcrv9el7TTE+1a5c3kZg3mm8xSPJSyLXtqCPXztF8gfabTavaUjhX22Vf 6 | c/nnKkA5XmFBQl2G6wi8IprVGx8ajpWn8oqI+s8Bb41oHrNX5XN4iLwPQlgy8G5y 7 | eZzYaLvKBn6QdPWHZuYXgwxok01I1EEuVPuBQwIDAQABAoIBAC0QzRLTKNO9cGtd 8 | 33y+v0rh3dik/hYmhEoUM0zqpc50jru1i4vgubTWw+oAbKRsOrx+UKR1rAaVE0nI 9 | PzLTAcn5qb7jiLlDNdueWAa5tQeaC08cMsEkwqa6+BijastSWID0NqbuYeoluqyo 10 | IGy4C5e5uNiehSMKuYodxNAhX4Wmm+BDvAB+hmXH5vGwaGHLP9jKjipQtVLOwReh 11 | uns4qsYbl1Y5XAy/myWCznfuEFcSO2uMhdQt1P8l69rCmnD9GjisgySk+TsP8GMj 12 | BCfhViS4iYBEkDDCOhzy40ebb9CqZRvnHZj2dzsVtfTPHEEw7+axcwwgp3INltP3 13 | S+r9vLkCgYEA8JNtPqwf+Hcsy2Vw9Ox01ROh6qH94zq6Yadn7eC5rI4jBocc4w8e 14 | wcAJ6hisO+/ZpDKaTtnfv/qZkdxQcKsDGtj8DHMmuvdGuGVB1Ou7O6jojexTBh/n 15 | zhYBYn7U4X+Qa+EjSW61LpaB2JEjlAQQB5WVMv0LwdlomGp8Ugodh4cCgYEAwaec 16 | WYrP62rNLUzbklLEOCzfl5ITXVaY31QzUabr6GO1EBTy6x/1PD8P8qybKgP/2/9c 17 | 9GsJhjXeCaD52CdM3mfZjJHHxUYq1AjPdUgvqpuaOgTD2RXt5YjjMxI8frJjPHZw 18 | EGr0Bv2rZAzNgYoJnu4lUAQHD/Xt81GVysNC72UCgYEArAqR/lazSklXlGEH5W9P 19 | oa03hoav/JbcAMfp79hcj8TZ0WpAQzRl52LA9FMOoJJGz0CMz0TJ2Q2KUx/9uoTh 20 | ZIq1vOWAVgZN4cIv0Od/bwu8uvm86c1ZRSzBcsGG1XgXPL61MGdmVFm3o7sveUNq 21 | fzH+j4ccqFUkkpLcMwMe2PMCgYAg6h8d7QUH62AyOeT8CstFDQN33KEj9SsIkmQi 22 | la91ETVeJWFga08gTUuloIbC7mkXeGDRPjwhLpitpEmy3oMnLhmA1epRcmqnHVQ9 23 | h+ZqJgZhpBuxkxObNtf+/zN/CUzqourLL5KIeace2zo2sUKPW8TFw9l7cf5eo9hE 24 | 3UlMLQKBgGNX5SnKnxPL1SU3TctIBj/OxRgo0VfD8El8zGtuswdu0HglA5C7Y18v 25 | TgQ9MiORVYRcWlVzccaG7l4wTQLWVnfoI3e7Sh2tiG5RNzAcv3SIZ3CFA7i+cl6+ 26 | 0fDshAORrSV9qwpnuwUIvASM7FKrxGLR9AtDzagAr85F1ZHTzw8b 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/utils/proxy.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const tls = require('tls'); 3 | const zlib = require('zlib'); 4 | const fs = require('fs'); 5 | 6 | class ProxyServer { 7 | constructor(authUser, authPass, serverHost, serverPort, proxyHttpHost, proxyHttpPort, proxyHttpsHost, proxyHttpsPort, httpsOptions = null) { 8 | this.AUTH_STRING = Buffer.from(`${authUser}:${authPass}`).toString('base64'); 9 | this.SERVER_HOST = serverHost; 10 | this.SERVER_PORT = serverPort; 11 | this.PROXY_HTTP_HOST = proxyHttpHost; 12 | this.PROXY_HTTP_PORT = proxyHttpPort; 13 | this.PROXY_HTTPS_HOST = proxyHttpsHost; 14 | this.PROXY_HTTPS_PORT = proxyHttpsPort; 15 | this.httpsOptions = httpsOptions; 16 | } 17 | 18 | isAuthenticated(dataString) { 19 | const authHeader = dataString.split('\r\n').find((line) => line.startsWith('Proxy-Authorization: ')); 20 | if (!authHeader) return false; 21 | 22 | const authToken = authHeader.split(' ')[2]; 23 | return authToken === this.AUTH_STRING; 24 | } 25 | 26 | decompressGzip(data, callback) { 27 | zlib.gunzip(data, (err, decompressed) => { 28 | if (err) { 29 | console.error('Failed to decompress data:', err); 30 | callback(err, null); 31 | } else { 32 | callback(null, decompressed.toString()); 33 | } 34 | }); 35 | } 36 | 37 | handleClientConnection(clientToProxySocket, isHttps) { 38 | console.log(`Client connected to proxy (${isHttps ? 'HTTPS' : 'HTTP'})`); 39 | 40 | clientToProxySocket.once('data', (data) => { 41 | const dataString = data.toString(); 42 | const isTLSConnection = dataString.indexOf('CONNECT') !== -1; 43 | 44 | console.log('Received data from client:', dataString); 45 | 46 | if (!this.isAuthenticated(dataString)) { 47 | console.log('Authentication failed'); 48 | clientToProxySocket.write('HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="Access to internal site"\r\n\r\n'); 49 | clientToProxySocket.end(); 50 | return; 51 | } 52 | 53 | console.log('Authentication successful'); 54 | 55 | if (isTLSConnection) { 56 | this.handleTlsConnection(clientToProxySocket, dataString); 57 | } else { 58 | this.handleHttpConnection(clientToProxySocket, data); 59 | } 60 | }); 61 | } 62 | 63 | handleTlsConnection(clientToProxySocket, dataString) { 64 | // eslint-disable-next-line no-unused-vars 65 | const [_, targetHost, targetPort] = dataString.split(' '); 66 | 67 | console.log(`Handling TLS connection to ${targetHost}:${targetPort}`); 68 | 69 | clientToProxySocket.write('HTTP/1.1 200 OK\r\n\r\n'); 70 | const proxyToServerSocket = net.createConnection( 71 | { 72 | host: targetHost.split(':')[0], 73 | port: targetPort.split(':')[0] 74 | }, 75 | () => { 76 | console.log('TLS connection established'); 77 | clientToProxySocket.pipe(proxyToServerSocket); 78 | proxyToServerSocket.pipe(clientToProxySocket); 79 | } 80 | ); 81 | 82 | proxyToServerSocket.on('error', (err) => { 83 | console.error('Proxy to server error (TLS):', err); 84 | }); 85 | 86 | clientToProxySocket.on('error', (err) => { 87 | console.error('Client to proxy error (TLS):', err); 88 | }); 89 | 90 | clientToProxySocket.on('close', () => { 91 | console.log('Client connection closed (TLS)'); 92 | }); 93 | 94 | proxyToServerSocket.on('close', () => { 95 | console.log('Server connection closed (TLS)'); 96 | }); 97 | } 98 | 99 | handleHttpConnection(clientToProxySocket, initialData) { 100 | console.log(`Connecting to server at ${this.SERVER_HOST}:${this.SERVER_PORT}`); 101 | 102 | const proxyToServerSocket = net.createConnection( 103 | { 104 | host: this.SERVER_HOST, 105 | port: this.SERVER_PORT 106 | }, 107 | () => { 108 | console.log('Proxy to server connection established'); 109 | proxyToServerSocket.write(initialData); 110 | clientToProxySocket.pipe(proxyToServerSocket); 111 | proxyToServerSocket.pipe(clientToProxySocket); 112 | } 113 | ); 114 | 115 | let serverResponseChunks = []; 116 | let headersReceived = false; 117 | let headers = ''; 118 | 119 | proxyToServerSocket.on('data', (chunk) => { 120 | serverResponseChunks.push(chunk); 121 | 122 | if (!headersReceived) { 123 | const chunkString = Buffer.concat(serverResponseChunks).toString(); 124 | const headerEndIndex = chunkString.indexOf('\r\n\r\n'); 125 | if (headerEndIndex !== -1) { 126 | headersReceived = true; 127 | headers = chunkString.slice(0, headerEndIndex + 4); 128 | console.log('Received headers from server:', headers); 129 | 130 | const body = chunk.slice(headerEndIndex + 4); 131 | serverResponseChunks = [body]; 132 | } 133 | } else { 134 | serverResponseChunks.push(chunk); 135 | } 136 | }); 137 | 138 | proxyToServerSocket.on('end', () => { 139 | console.log('Server connection ended'); 140 | 141 | const responseBuffer = Buffer.concat(serverResponseChunks); 142 | const contentEncodingHeader = headers.toLowerCase().includes('content-encoding: gzip'); 143 | 144 | if (contentEncodingHeader) { 145 | this.decompressGzip(responseBuffer, (err, decompressedData) => { 146 | if (!err) { 147 | console.log('Decompressed server data:', decompressedData); 148 | } else { 149 | console.error('Failed to decompress server data:', err); 150 | } 151 | }); 152 | } else { 153 | console.log('Received data from server:', responseBuffer.toString()); 154 | } 155 | 156 | clientToProxySocket.write(headers); 157 | clientToProxySocket.write(responseBuffer); 158 | clientToProxySocket.end(); 159 | }); 160 | 161 | proxyToServerSocket.on('close', () => { 162 | console.log('Server connection closed'); 163 | }); 164 | 165 | proxyToServerSocket.on('error', (err) => { 166 | console.error('Proxy to server error:', err); 167 | }); 168 | 169 | clientToProxySocket.on('end', () => { 170 | console.log('Client connection ended'); 171 | }); 172 | 173 | clientToProxySocket.on('close', () => { 174 | console.log('Client connection closed'); 175 | }); 176 | 177 | clientToProxySocket.on('error', (err) => { 178 | console.error('Client to proxy error:', err); 179 | }); 180 | } 181 | 182 | start() { 183 | const httpServer = net.createServer((socket) => this.handleClientConnection(socket, false)); 184 | const httpsServer = this.httpsOptions ? tls.createServer(this.httpsOptions, (socket) => this.handleClientConnection(socket, true)) : null; 185 | 186 | httpServer.on('error', (err) => { 187 | console.error('Internal HTTP server error occurred:', err); 188 | }); 189 | 190 | if (httpsServer) { 191 | httpsServer.on('error', (err) => { 192 | console.error('Internal HTTPS server error occurred:', err); 193 | }); 194 | } 195 | 196 | httpServer.listen( 197 | { 198 | host: this.PROXY_HTTP_HOST, 199 | port: this.PROXY_HTTP_PORT 200 | }, 201 | () => { 202 | console.log(`HTTP Server listening on ${this.PROXY_HTTP_HOST}:${this.PROXY_HTTP_PORT}`); 203 | } 204 | ); 205 | 206 | if (httpsServer) { 207 | httpsServer.listen( 208 | { 209 | host: this.PROXY_HTTPS_HOST, 210 | port: this.PROXY_HTTPS_PORT 211 | }, 212 | () => { 213 | console.log(`HTTPS Server listening on ${this.PROXY_HTTPS_HOST}:${this.PROXY_HTTPS_PORT}`); 214 | } 215 | ); 216 | } 217 | } 218 | } 219 | 220 | // HTTPS options (certificates) 221 | const httpsOptions = { 222 | key: fs.readFileSync('./test/utils/private-key.pem'), 223 | cert: fs.readFileSync('./test/utils/certificate.pem') 224 | }; 225 | 226 | const proxyServer = new ProxyServer('user', 'password', '127.0.0.1', 8088, '0.0.0.0', 8080, '0.0.0.0', 8443, httpsOptions); 227 | proxyServer.start(); 228 | --------------------------------------------------------------------------------