├── .browserslistrc ├── .dist.babelrc ├── .dist.eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── codesee-arch-diagram.yml │ └── mattermost-ziti-webhook.yml ├── .gitignore ├── .lib.babelrc ├── .lib.eslintrc ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── channel │ ├── channel-options.js │ ├── channel.js │ ├── connection-options.js │ ├── connection.js │ ├── connections.js │ ├── controller-channel-options.js │ ├── controller-channel.js │ ├── header-options.js │ ├── header.js │ ├── messages.js │ ├── protocol.js │ ├── tls-connection-options.js │ └── tls-connection.js ├── client.js ├── constants.js ├── context │ ├── context.js │ ├── contexttypes.js │ ├── controller-client.js │ ├── controller-ws-client.js │ ├── options.js │ └── servicetypes.js ├── enroll │ ├── atob.js │ ├── base64.js │ ├── base64_url_decode.js │ ├── enroller.js │ └── options.js ├── http │ ├── _http_client.js │ ├── _http_common.js │ ├── _http_incoming.js │ ├── _http_outgoing.js │ ├── blob.js │ ├── body.js │ ├── browser-stdout.js │ ├── buffer-util.js │ ├── constants.js │ ├── event-target.js │ ├── file-reader-stream.js │ ├── file-reader.js │ ├── form-data.js │ ├── freelist.js │ ├── headers.js │ ├── http-parser.js │ ├── http.js │ ├── internal │ │ ├── constants.js │ │ ├── errors.js │ │ ├── http.js │ │ ├── querystring.js │ │ ├── util.js │ │ └── util │ │ │ └── types.js │ ├── limiter.js │ ├── permessage-deflate.js │ ├── populate.js │ ├── receiver.js │ ├── request.js │ ├── response.js │ ├── sender.js │ ├── validation.js │ ├── ziti-agent.js │ ├── ziti-socket.js │ ├── ziti-websocket-wrapper.js │ └── ziti-xhr.js ├── index.js ├── logLevels.js ├── pki │ ├── options.js │ └── pki.js ├── ui │ └── identity_modal │ │ ├── css.js │ │ ├── dragdrop.js │ │ ├── file-parser.js │ │ ├── keypair_generation_html.js │ │ ├── keypair_generation_msg.js │ │ ├── reloading_page_html.js │ │ ├── select.js │ │ ├── updb_prompt_html.js │ │ └── updb_prompt_keypairDirectory_html.js ├── updb │ ├── error.js │ ├── keypairDirectory.js │ ├── login.js │ ├── options.js │ └── updb.js ├── utils │ ├── localstorage.js │ ├── pki.js │ ├── throwif.js │ ├── utils.js │ └── ziti-reporter.js └── websocket │ ├── options.js │ ├── requests.js │ ├── robust-websocket.js │ └── websocket.js └── tests ├── karma ├── client.js ├── index.js └── utils.js ├── mock-localstorage.js └── unit ├── index.js └── utils.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | > 1% 4 | last 2 versions 5 | ie 9 6 | -------------------------------------------------------------------------------- /.dist.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "useBuiltIns": "usage", 5 | "corejs": 3, 6 | "targets": { 7 | "browsers": [ "> 1%", "last 2 versions", "ie 9" ] 8 | } 9 | }] 10 | ], 11 | "plugins": [ 12 | ["@babel/plugin-transform-regenerator", { 13 | "asyncGenerators": true, 14 | "generators": true, 15 | "async": true 16 | }] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.dist.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "env": { 4 | "node": false, 5 | "browser": true, 6 | "amd": true, 7 | "es6": true 8 | }, 9 | "plugins": ["compat"], 10 | "rules": { 11 | "compat/compat": "error", 12 | "no-console": "off", 13 | "no-empty": "off", 14 | "no-extra-semi": "off", 15 | "no-func-assign": "off", 16 | "no-undef": "off", 17 | "no-unused-vars": "off", 18 | "no-useless-escape": "off", 19 | "no-cond-assign": "off", 20 | "no-redeclare": "off" 21 | }, 22 | "globals": { 23 | "regeneratorRuntime": "writable" 24 | }, 25 | "settings": { 26 | "polyfills": [ 27 | "Promise", 28 | "Array.from", 29 | "Symbol", 30 | "Object.getOwnPropertySymbols", 31 | "Object.setPrototypeOf" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | paths-ignore: 10 | - 'package.json' 11 | - 'CHANGELOG.md' 12 | pull_request: 13 | branches: [ main ] 14 | workflow_dispatch: 15 | inputs: 16 | tags: 17 | required: false 18 | description: 'Misc tags' 19 | 20 | jobs: 21 | 22 | # ------------------------------------------------------------------------------- 23 | # Do a clean build, test, and publish 24 | # ------------------------------------------------------------------------------- 25 | build: 26 | 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | matrix: 31 | node-version: [14.x] 32 | 33 | steps: 34 | 35 | - name: Checkout 36 | uses: actions/checkout@v2 37 | with: 38 | token: ${{ secrets.GH_ACTION }} 39 | 40 | - name: Bump semver 41 | if: github.ref == 'refs/heads/main' 42 | uses: TriPSs/conventional-changelog-action@v3 43 | with: 44 | github-token: ${{ secrets.GH_ACTION }} 45 | git-message: 'chore(release): {version}' 46 | preset: 'angular' 47 | tag-prefix: 'v' 48 | output-file: 'CHANGELOG.md' 49 | skip-on-empty: true # do not alter semver when we push 'chore: ...' commits 50 | release-count: 0 # ensure changelog is generated to contain ALL updates 51 | 52 | - name: Pull newly bumped semver 53 | if: github.ref == 'refs/heads/main' 54 | run: git pull 55 | 56 | - name: Setup .npmrc 57 | if: github.ref == 'refs/heads/main' 58 | # Setup .npmrc file to prepare for possible publish to npm 59 | uses: actions/setup-node@v1 60 | with: 61 | node-version: ${{ matrix.node-version }} 62 | registry-url: 'https://registry.npmjs.org' 63 | 64 | - name: Install 65 | run: npm ci 66 | 67 | - name: Build 68 | run: npm run build 69 | 70 | - name: Test 71 | run: npm test 72 | 73 | - name: Unit-test Coverage 74 | run: npm run coverage 75 | 76 | - name: Unit-test Coverage Report 77 | run: npm run coverage-report 78 | 79 | # not yet 80 | # - name: Publish beta release 81 | # run: npm publish --tag next --access public 82 | # if: github.ref != 'refs/heads/main' 83 | # env: 84 | # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 85 | 86 | - name: Publish production release 87 | if: github.ref == 'refs/heads/main' 88 | run: npm publish --access public 89 | env: 90 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 91 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 5 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request_target: 6 | types: [opened, synchronize, reopened] 7 | 8 | name: CodeSee Map 9 | 10 | jobs: 11 | test_map_action: 12 | runs-on: ubuntu-latest 13 | continue-on-error: true 14 | name: Run CodeSee Map Analysis 15 | steps: 16 | - name: checkout 17 | id: checkout 18 | uses: actions/checkout@v2 19 | with: 20 | repository: ${{ github.event.pull_request.head.repo.full_name }} 21 | ref: ${{ github.event.pull_request.head.ref }} 22 | fetch-depth: 0 23 | 24 | # codesee-detect-languages has an output with id languages. 25 | - name: Detect Languages 26 | id: detect-languages 27 | uses: Codesee-io/codesee-detect-languages-action@latest 28 | 29 | - name: Configure JDK 16 30 | uses: actions/setup-java@v2 31 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).java }} 32 | with: 33 | java-version: '16' 34 | distribution: 'zulu' 35 | 36 | # CodeSee Maps Go support uses a static binary so there's no setup step required. 37 | 38 | - name: Configure Node.js 14 39 | uses: actions/setup-node@v2 40 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }} 41 | with: 42 | node-version: '14' 43 | 44 | - name: Configure Python 3.x 45 | uses: actions/setup-python@v2 46 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).python }} 47 | with: 48 | python-version: '3.x' 49 | architecture: 'x64' 50 | 51 | - name: Configure Ruby '3.x' 52 | uses: ruby/setup-ruby@v1 53 | if: ${{ fromJSON(steps.detect-languages.outputs.languages).ruby }} 54 | with: 55 | ruby-version: '3.0' 56 | 57 | # CodeSee Maps Rust support uses a static binary so there's no setup step required. 58 | 59 | - name: Generate Map 60 | id: generate-map 61 | uses: Codesee-io/codesee-map-action@latest 62 | with: 63 | step: map 64 | github_ref: ${{ github.ref }} 65 | languages: ${{ steps.detect-languages.outputs.languages }} 66 | 67 | - name: Upload Map 68 | id: upload-map 69 | uses: Codesee-io/codesee-map-action@latest 70 | with: 71 | step: mapUpload 72 | api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 73 | github_ref: ${{ github.ref }} 74 | 75 | - name: Insights 76 | id: insights 77 | uses: Codesee-io/codesee-map-action@latest 78 | with: 79 | step: insights 80 | api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 81 | github_ref: ${{ github.ref }} 82 | -------------------------------------------------------------------------------- /.github/workflows/mattermost-ziti-webhook.yml: -------------------------------------------------------------------------------- 1 | name: mattermost-ziti-webhook 2 | on: 3 | create: 4 | delete: 5 | issues: 6 | issue_comment: 7 | pull_request_review: 8 | pull_request_review_comment: 9 | pull_request: 10 | push: 11 | fork: 12 | release: 13 | 14 | jobs: 15 | mattermost-ziti-webhook: 16 | runs-on: macos-latest 17 | name: POST Webhook 18 | env: 19 | ZITI_LOG: 99 20 | ZITI_NODEJS_LOG: 99 21 | steps: 22 | - uses: openziti/ziti-webhook-action@main 23 | with: 24 | ziti-id: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} 25 | webhook-url: ${{ secrets.ZITI_MATTERMOST_WEBHOOK_URL }} 26 | webhook-secret: ${{ secrets.ZITI_MATTERMOSTI_WEBHOOK_SECRET }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage.html 3 | .DS_Store 4 | .VSCodeCounter/ 5 | node_modules 6 | *.sock 7 | test.js 8 | .idea 9 | *.log 10 | coverage 11 | .nyc_output 12 | lib 13 | dist 14 | *.swp 15 | -------------------------------------------------------------------------------- /.lib.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", { 4 | "useBuiltIns": "usage", 5 | "corejs": 3, 6 | "targets": { 7 | "browsers": [ "> 1%", "last 2 versions", "ie 9" ] 8 | } 9 | }] 10 | ], 11 | "plugins": [ 12 | ["@babel/plugin-transform-regenerator", { 13 | "asyncGenerators": true, 14 | "generators": true, 15 | "async": true 16 | }] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.lib.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:node/recommended"], 3 | "env": { 4 | "browser": true 5 | }, 6 | "rules": { 7 | "node/no-deprecated-api": "off", 8 | "no-console": "off", 9 | "no-unused-vars": "off", 10 | "no-empty": "off", 11 | "node/no-unsupported-features/node-builtins": "off", 12 | "no-func-assign": "off", 13 | "no-global-assign": ["error", {"exceptions": ["exports"]}] 14 | }, 15 | "overrides": [ 16 | { 17 | "files": [ "lib/client.js" ], 18 | "globals": { 19 | "ActiveXObject": "readable" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | lib-cov 6 | coverage.html 7 | bower.json 8 | coverage 9 | src 10 | .travis.yml 11 | .nojekyll 12 | .nyc_output 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All open source projects managed by OpenZiti share a common [code of conduct](https://docs.openziti.io/policies/CODE_OF_CONDUCT.html) 4 | which all contributors are expected to follow. Please be sure you read, understand and adhere to the guidelines expressed therein. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | NetFoundry welcomes all and any contributions. All open source projects managed by NetFoundry share a common 4 | [guide for contributions](https://docs.openziti.io/policies/CONTRIBUTING.html). 5 | 6 | If you are eager to contribute to a NetFoundry-managed open source project please read and act accordingly. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚨 Ziti SDK for JavaScript - Archived and No Longer Maintained 🚨 2 | 3 | ## ⚠️ Repository Status: **Defunct** 4 | This repository is no longer maintained or supported as of 2021-11-11. No further updates, bug fixes, or support will be provided for the Ziti SDK for JavaScript. 5 | 6 | --- 7 | 8 | ## Why Was This Archived? 9 | The Ziti SDK for JavaScript has been discontinued and will be replaced by a future `ziti-sdk-browser`. 10 | 11 | --- 12 | 13 | ## Recommendations 14 | If you are seeking alternatives or similar functionality, consider: 15 | 16 | - **[OpenZiti SDKs](https://github.com/openziti):** Explore other actively maintained SDKs within the OpenZiti ecosystem. 17 | 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Until v1.0.0 or higher is reached, only the most recent version is supported. After v1.0.0 a new version support statement will be released. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you have an issue that is not a sensitive security issue, please submit it to the GitHub issue tracker on this repository 10 | or if you're not sure what repository to submit it under, use the [main OpenZiti/ziti repository](https://github.com/openziti/ziti/issues). 11 | 12 | If you have a sensitive security issue or are unsure if it is sensitive, please email it to: security@openziti.org 13 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Sep 28 2020 13:48:35 GMT-0400 (Eastern Daylight Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | // provide enough time to facilitate interactive debugging in chrome devtools 11 | client: { 12 | mocha: { 13 | timeout : 120000 // 2 mins - upped from 2 seconds 14 | } 15 | }, 16 | 17 | // frameworks to use 18 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 19 | frameworks: ['mocha', 'browserify'], 20 | 21 | 22 | // list of files / patterns to load in the browser 23 | files: [ 24 | 25 | { pattern: 'tests/karma/index.js', watched: true, included: true, served: true }, 26 | 27 | // { pattern: 'src/*.js', included: true }, 28 | // { pattern: 'tests/**/*.js', included: false } 29 | 30 | ], 31 | 32 | 33 | // list of files / patterns to exclude 34 | exclude: [ 35 | ], 36 | 37 | 38 | // preprocess matching files before serving them to the browser 39 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 40 | preprocessors: { 41 | 'src/**/*.js': [ 'browserify' , 'coverage' ], 42 | 'tests/**/*.js': [ 'browserify' , 'coverage' ] 43 | }, 44 | 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress' 48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 49 | reporters: ['progress', 'coverage'], 50 | 51 | 52 | coverageReporter: { 53 | 54 | reporters: [ 55 | { type: 'text' }, 56 | { type: 'html', subdir: 'report-html' }, 57 | { type: 'lcov', subdir: 'report-lcov' }, 58 | ], 59 | 60 | }, 61 | 62 | 63 | // web server port 64 | port: 9876, 65 | 66 | 67 | // enable / disable colors in the output (reporters and logs) 68 | colors: true, 69 | 70 | 71 | // level of logging 72 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 73 | logLevel: config.LOG_INFO, 74 | 75 | 76 | // enable / disable watching file and executing tests whenever any file changes 77 | autoWatch: true, 78 | 79 | 80 | // start these browsers 81 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 82 | browsers: ['Chrome'], 83 | 84 | 85 | // Continuous Integration mode 86 | // if true, Karma captures browsers, runs the tests and exits 87 | singleRun: true, 88 | // singleRun: false, 89 | 90 | // Concurrency level 91 | // how many browser should be started simultaneous 92 | concurrency: Infinity 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openziti/ziti-sdk-js", 3 | "version": "0.15.4", 4 | "description": "A JavaScript-based SDK for delivering secure browser-based web applications over a Ziti Network", 5 | "scripts": { 6 | "test": "cross-env NODE_ENV=test mocha -t 30000 -R ${REPORTER:-spec} tests/unit/index.js", 7 | "test-karma": "karma start", 8 | "coverage": "rm -rf coverage && nyc --reporter=lcov --reporter=text-summary npm test", 9 | "coverage-report": "nyc report", 10 | "browserify": "browserify src/node/index.js -o dist/ziti.js -s ziti", 11 | "build": "npm run build:clean && npm run build:lib && npm run build:dist", 12 | "build:clean": "rimraf lib dist", 13 | "build:lib": "babel --config-file ./.lib.babelrc src --out-dir lib", 14 | "build:dist": "npm run browserify && npm run minify", 15 | "minify": "terser --compress --mangle -o dist/ziti.min.js dist/ziti.js" 16 | }, 17 | "nyc": { 18 | "exclude": [ 19 | "tests" 20 | ] 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/openziti/ziti-sdk-js.git" 25 | }, 26 | "keywords": [ 27 | "ziti", 28 | "js", 29 | "javascript", 30 | "browser" 31 | ], 32 | "author": "NetFoundry", 33 | "license": "Apache-2.0", 34 | "bugs": { 35 | "url": "https://github.com/openziti/ziti-sdk-js/issues" 36 | }, 37 | "homepage": "https://github.com/openziti/ziti-sdk-js#readme", 38 | "lint-staged": { 39 | "linters": { 40 | "*.js": [ 41 | "xo --fix", 42 | "git add" 43 | ], 44 | "*.md": [ 45 | "remark . -qfo", 46 | "git add" 47 | ], 48 | "package.json": [ 49 | "fixpack", 50 | "git add" 51 | ] 52 | } 53 | }, 54 | "main": "./dist/ziti.js", 55 | "browser": { 56 | "./dist/ziti.js": "./dist/ziti.js", 57 | "./src/node/index.js": "./src/index.js" 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.16.0", 61 | "@babel/core": "^7.16.0", 62 | "@babel/plugin-transform-regenerator": "^7.13.15", 63 | "@babel/preset-env": "^7.14.2", 64 | "babelify": "^10.0.0", 65 | "browserify": "^16.5.2", 66 | "cross-env": "^7.0.2", 67 | "elliptic": "^6.5.4", 68 | "karma": "^6.3.2", 69 | "karma-browserify": "^8.1.0", 70 | "karma-chrome-launcher": "^3.1.0", 71 | "karma-coverage": "^2.0.3", 72 | "karma-mocha": "^2.0.1", 73 | "karma-mocha-reporter": "^2.2.5", 74 | "mocha": "^9.1.3", 75 | "mocha-lcov-reporter": "^1.3.0", 76 | "mock-local-storage": "^1.1.15", 77 | "nyc": "^15.1.0", 78 | "rimraf": "^3.0.2", 79 | "tinyify": "^3.0.0", 80 | "watchify": "^4.0.0" 81 | }, 82 | "dependencies": { 83 | "@babel/runtime": "^7.14.0", 84 | "@types/lodash.isnull": "^3.0.6", 85 | "arraybuffer-to-string": "^1.0.2", 86 | "asn1js": "^2.0.26", 87 | "assert": "^2.0.0", 88 | "async-mutex": "^0.3.1", 89 | "asynckit": "^0.4.0", 90 | "browserify-zlib": "^0.2.0", 91 | "buffer": "^5.6.1", 92 | "bufferutil": "^4.0.3", 93 | "chnl": "^1.2.0", 94 | "combined-stream": "^1.0.8", 95 | "consola": "^2.15.0", 96 | "cookie-interceptor": "^1.0.0", 97 | "create-hash": "^1.2.0", 98 | "drag-drop": "^6.0.2", 99 | "events": "^3.3.0", 100 | "flat-options": "^0.1.3", 101 | "format-message": "^6.2.3", 102 | "from2": "^2.3.0", 103 | "html-select": "^2.3.24", 104 | "html-tokenize": "^2.0.1", 105 | "js-cookie": "^2.2.1", 106 | "jwt-decode": "^3.1.2", 107 | "libsodium-wrappers": "^0.7.8", 108 | "localforage": "^1.9.0", 109 | "lodash.concat": "^4.5.0", 110 | "lodash.filter": "^4.6.0", 111 | "lodash.find": "^4.6.0", 112 | "lodash.foreach": "^4.5.0", 113 | "lodash.has": "^4.5.2", 114 | "lodash.isequal": "^4.5.0", 115 | "lodash.isnull": "^3.0.0", 116 | "lodash.isundefined": "^3.0.1", 117 | "lodash.minby": "^4.6.0", 118 | "lodash.result": "^4.5.2", 119 | "lodash.split": "^4.4.2", 120 | "lodash.tonumber": "^4.0.3", 121 | "micromodal": "^0.4.6", 122 | "multistream": "^4.1.0", 123 | "node-forge": "github:githoniel/forge", 124 | "pkijs": "^2.1.90", 125 | "promise-controller": "^1.0.0", 126 | "promise.prototype.finally": "^3.1.2", 127 | "q": "^1.5.1", 128 | "randombytes": "^2.1.0", 129 | "readable-stream": "^3.6.0", 130 | "robust-websocket": "^1.0.0", 131 | "stream-browserify": "^3.0.0", 132 | "through2": "^4.0.2", 133 | "typedarray-to-buffer": "^4.0.0", 134 | "url": "^0.11.0", 135 | "utf-8-validate": "^5.0.4", 136 | "uuid": "^8.3.2" 137 | } 138 | } -------------------------------------------------------------------------------- /src/channel/channel-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | /** 19 | * Default options. 20 | */ 21 | 22 | 23 | module.exports = { 24 | 25 | /** 26 | * See {@link Options.ctx} 27 | * 28 | */ 29 | ctx: null, 30 | 31 | /** 32 | * See {@link Options.session_token} 33 | * 34 | */ 35 | session_token: null, 36 | 37 | /** 38 | * See {@link Options.network_session_token} 39 | * 40 | */ 41 | network_session_token: null, 42 | 43 | /** 44 | * See {@link Options.data} 45 | * 46 | */ 47 | data: null, 48 | 49 | /** 50 | * See {@link Options.edgeRouter} 51 | * 52 | */ 53 | edgeRouter: null, 54 | 55 | /** 56 | * See {@link Options.timeout} 57 | */ 58 | timeout: 0, 59 | 60 | /** 61 | * See {@link Options.helloTimeout} 62 | */ 63 | helloTimeout: 0, 64 | 65 | }; -------------------------------------------------------------------------------- /src/channel/connection-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | /** 19 | * Default options. 20 | */ 21 | 22 | 23 | module.exports = { 24 | 25 | /** 26 | * See {@link Options.ctx} 27 | * 28 | */ 29 | ctx: null, 30 | 31 | /** 32 | * See {@link Options.data} 33 | * 34 | */ 35 | data: null, 36 | 37 | /** 38 | * See {@link Options.edgeRouterHost} 39 | * 40 | */ 41 | edgeRouterHost: null, 42 | 43 | }; -------------------------------------------------------------------------------- /src/channel/connection.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Module dependencies. 19 | */ 20 | 21 | const flatOptions = require('flat-options'); 22 | 23 | const defaultOptions = require('./connection-options'); 24 | const edge_protocol = require('./protocol'); 25 | const Messages = require('./messages'); 26 | 27 | 28 | /** 29 | * @typicalname connection 30 | */ 31 | module.exports = class ZitiConnection { 32 | 33 | /** 34 | * 35 | * @param {Options} [options] 36 | */ 37 | constructor(options) { 38 | 39 | this._options = flatOptions(options, defaultOptions); 40 | 41 | this._ctx = this._options.ctx; 42 | 43 | this._data = this._options.data; 44 | 45 | this._state = edge_protocol.conn_state.Initial; 46 | 47 | this._timeout = this._ctx.getTimeout(); 48 | 49 | this._edgeMsgSeq = 1; 50 | 51 | this._id = this._ctx.getNextConnectionId(); 52 | 53 | this._messages = new Messages({ ctx: this._ctx, conn: this }); 54 | 55 | } 56 | 57 | getCtx() { 58 | return this._ctx; 59 | } 60 | 61 | getData() { 62 | return this._data; 63 | } 64 | 65 | getMessages() { 66 | return this._messages; 67 | } 68 | 69 | getState() { 70 | return this._state; 71 | } 72 | setState(state) { 73 | this._state = state; 74 | } 75 | 76 | getId() { 77 | return this._id; 78 | } 79 | 80 | getAndIncrementSequence() { 81 | let seq = this._edgeMsgSeq; 82 | this._edgeMsgSeq++; 83 | return seq; 84 | } 85 | 86 | getSocket() { 87 | return this._socket; 88 | } 89 | setSocket(socket) { 90 | this._socket = socket; 91 | } 92 | getDataCallback() { 93 | return this._dataCallback; 94 | } 95 | setDataCallback(fn) { 96 | this._dataCallback = fn; 97 | } 98 | 99 | getChannel() { 100 | return this._channel; 101 | } 102 | setChannel(channel) { 103 | this._channel = channel; 104 | } 105 | 106 | getEncrypted() { 107 | return this._encrypted; 108 | } 109 | setEncrypted(encrypted) { 110 | this._encrypted = encrypted; 111 | } 112 | 113 | getCryptoEstablishComplete() { 114 | return this._cryptoEstablishComplete; 115 | } 116 | setCryptoEstablishComplete(complete) { 117 | this._cryptoEstablishComplete = complete; 118 | } 119 | 120 | getKeypair() { 121 | return this._keypair; 122 | } 123 | setKeypair(keypair) { 124 | this._keypair = keypair; 125 | } 126 | 127 | getSharedRx() { 128 | return this._sharedRx; 129 | } 130 | setSharedRx(sharedRx) { 131 | this._sharedRx = sharedRx; 132 | } 133 | 134 | getSharedTx() { 135 | return this._sharedTx; 136 | } 137 | setSharedTx(sharedTx) { 138 | this._sharedTx = sharedTx; 139 | } 140 | 141 | getCrypt_o() { 142 | return this._crypt_o; 143 | } 144 | setCrypt_o(crypt_o) { 145 | this._crypt_o = crypt_o; 146 | } 147 | 148 | getCrypt_i() { 149 | return this._crypt_i; 150 | } 151 | setCrypt_i(crypt_i) { 152 | this._crypt_i = crypt_i; 153 | } 154 | 155 | } -------------------------------------------------------------------------------- /src/channel/connections.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Class to manage connections. 19 | * @private 20 | */ 21 | 22 | 23 | module.exports = class ZitiConnections { 24 | constructor() { 25 | this._items = new Map(); 26 | } 27 | 28 | _saveConnection(conn) { 29 | this._items.set(conn.getId(), conn); 30 | } 31 | 32 | _deleteConnection(conn) { 33 | this._items.delete(conn.getId()); 34 | } 35 | 36 | _getConnection(connId) { 37 | if (this._items.has(connId)) { 38 | return this._items.get(connId); 39 | } else { 40 | return undefined; 41 | } 42 | } 43 | 44 | }; -------------------------------------------------------------------------------- /src/channel/controller-channel-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | /** 19 | * Default options. 20 | */ 21 | 22 | 23 | module.exports = { 24 | 25 | /** 26 | * See {@link Options.ctx} 27 | * 28 | */ 29 | ctx: null, 30 | 31 | /** 32 | * See {@link Options.data} 33 | * 34 | */ 35 | data: null, 36 | 37 | /** 38 | * See {@link Options.controllerHost} 39 | * 40 | */ 41 | controllerHost: null, 42 | 43 | /** 44 | * See {@link Options.controllerPort} 45 | * 46 | */ 47 | controllerPort: 80, 48 | 49 | /** 50 | * See {@link Options.timeout} 51 | */ 52 | timeout: 0, 53 | 54 | /** 55 | * See {@link Options.helloTimeout} 56 | */ 57 | helloTimeout: 0, 58 | 59 | }; -------------------------------------------------------------------------------- /src/channel/header-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Default options. 19 | */ 20 | 21 | const LogLevel = require('../logLevels'); 22 | 23 | 24 | module.exports = { 25 | 26 | /** 27 | * See {@link Options.headerType} 28 | */ 29 | headerType: null, 30 | 31 | /** 32 | * See {@link Options.headerData} 33 | */ 34 | headerData: null, 35 | 36 | }; -------------------------------------------------------------------------------- /src/channel/header.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Module dependencies. 19 | */ 20 | 21 | const formatMessage = require('format-message'); 22 | const flatOptions = require('flat-options'); 23 | const isNull = require('lodash.isnull'); 24 | const isEqual = require('lodash.isequal'); 25 | 26 | const utils = require('../utils/utils'); 27 | const defaultOptions = require('./header-options'); 28 | const {throwIf} = require('../utils/throwif'); 29 | const edge_protocol = require('./protocol'); 30 | 31 | 32 | formatMessage.setup({ 33 | // locale: 'es-ES', // what locale strings should be displayed 34 | // missingReplacement: '!!NOT TRANSLATED!!', // use this when a translation is missing instead of the default message 35 | missingTranslation: 'ignore', // don't console.warn or throw an error when a translation is missing 36 | }) 37 | 38 | 39 | 40 | /** 41 | * @typicalname header 42 | */ 43 | module.exports = class Header { 44 | /** 45 | * 46 | * @param {int} headerId ZitiEnums.header_id 47 | * @param {Options} [options] 48 | */ 49 | constructor(headerId, options) { 50 | this._headerId = headerId; 51 | this._options = flatOptions(options, defaultOptions); 52 | 53 | throwIf(isNull(this._options.headerType), formatMessage('headerType not specified')); 54 | this._headerType = this._options.headerType; 55 | 56 | throwIf(isNull(this._options.headerData), formatMessage('headerData not specified')); 57 | this._headerData = this._options.headerData; 58 | 59 | this._bytesForWire = this._createBytesForWire(); 60 | 61 | this._length = this._bytesForWire.length; 62 | } 63 | 64 | getId() { 65 | return this._headerId; 66 | } 67 | 68 | getData() { 69 | return this._headerData; 70 | } 71 | 72 | getLength() { 73 | return this._length; 74 | } 75 | 76 | getBytesForWire() { 77 | return this._bytesForWire; 78 | } 79 | 80 | _createBytesForWire() { 81 | 82 | if (isEqual(this._headerType, edge_protocol.header_type.StringType)) { 83 | 84 | let headerDataLength = Buffer.byteLength(this._headerData, 'utf8'); 85 | 86 | let bytes_header_id_and_length = new Buffer( 4 + 4 ); 87 | bytes_header_id_and_length.writeUInt32LE(this._headerId, 0); 88 | bytes_header_id_and_length.writeUInt32LE(headerDataLength, 4); 89 | 90 | 91 | let bytes_header_data = utils.toUTF8Array(this._headerData); 92 | let buffer_header_data = Buffer.from(bytes_header_data); 93 | 94 | let bytes_complete_header = Buffer.concat([bytes_header_id_and_length, buffer_header_data], 4 + 4 + headerDataLength ); 95 | 96 | return bytes_complete_header; 97 | 98 | } else if (isEqual(this._headerType, edge_protocol.header_type.IntType)) { 99 | 100 | let headerDataLength = 4; 101 | 102 | let bytes_complete_header = new Buffer( 4 + 4 + 4 ); 103 | bytes_complete_header.writeUInt32LE(this._headerId, 0); 104 | bytes_complete_header.writeUInt32LE(headerDataLength, 4); 105 | bytes_complete_header.writeInt32LE(this._headerData, 8); 106 | 107 | return bytes_complete_header; 108 | 109 | } else if (isEqual(this._headerType, edge_protocol.header_type.Uint8ArrayType)) { 110 | 111 | let headerDataLength = Buffer.byteLength(this._headerData, 'utf8'); 112 | 113 | let bytes_header_id_and_length = new Buffer( 4 + 4 ); 114 | bytes_header_id_and_length.writeUInt32LE(this._headerId, 0); 115 | bytes_header_id_and_length.writeUInt32LE(headerDataLength, 4); 116 | 117 | let buffer_header_data = Buffer.from(this._headerData); 118 | 119 | let bytes_complete_header = Buffer.concat([bytes_header_id_and_length, buffer_header_data], 4 + 4 + headerDataLength ); 120 | 121 | return bytes_complete_header; 122 | 123 | } else { 124 | 125 | throw new Error('unknown headerType'); 126 | 127 | } 128 | } 129 | 130 | _createFromBytesFromWire(bytes) { 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/channel/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Class for manage pending messages. 19 | * @private 20 | */ 21 | 22 | const PromiseController = require('promise-controller'); 23 | const promiseFinally = require('promise.prototype.finally'); 24 | const isNull = require('lodash.isnull'); 25 | 26 | 27 | module.exports = class Messages { 28 | constructor(options) { 29 | this._items = new Map(); 30 | this._ctx = options.ctx; 31 | this._conn = options.conn; 32 | this._channel = options.channel; 33 | } 34 | 35 | /** 36 | * Creates new message and stores it in the list. 37 | * 38 | * @param {String|Number} messageId 39 | * @param {Function} fn 40 | * @param {Number} timeout 41 | * @returns {Promise} 42 | */ 43 | create(messageId, fn, timeout) { 44 | this._ctx.logger.trace("messages.create(): conn[%d] messageId[%o]", (this._conn ? this._conn.getId() : 'n/a'), messageId); 45 | this._rejectExistingMessage(messageId); 46 | return this._createNewMessage(messageId, fn, timeout); 47 | } 48 | 49 | resolve(messageId, data) { 50 | this._ctx.logger.trace("messages.resolve(): conn[%d] messageId[%o] data[%o]", (this._conn ? this._conn.getId() : 'n/a'), messageId, data); 51 | if (!isNull(messageId) && this._items.has(messageId)) { 52 | this._ctx.logger.trace("messages.resolve(): FOUND messageId: [%o]", messageId); 53 | this._items.get(messageId).resolve(data); 54 | } 55 | } 56 | 57 | rejectAll(error) { 58 | this._items.forEach(message => message.isPending ? message.reject(error) : null); 59 | } 60 | 61 | _rejectExistingMessage(messageId) { 62 | const existingMessage = this._items.get(messageId); 63 | if (existingMessage && existingMessage .isPending) { 64 | existingMessage .reject(new Error(`message is replaced, messageId: ${messageId}`)); 65 | } 66 | } 67 | 68 | _createNewMessage(messageId, fn, timeout) { 69 | const message = new PromiseController({ 70 | timeout, 71 | timeoutReason: `message was rejected by timeout (${timeout} ms). messageId: ${messageId}` 72 | }); 73 | this._items.set(messageId, message); 74 | return promiseFinally(message.call(fn), () => this._deleteMessage (messageId, message)); 75 | } 76 | 77 | _deleteMessage (messageId, message) { 78 | // this check is important when message was replaced 79 | if (this._items.get(messageId) === message) { 80 | this._items.delete(messageId); 81 | } 82 | } 83 | }; -------------------------------------------------------------------------------- /src/channel/protocol.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | 19 | const ZitiEnums = { 20 | 21 | VERSION: [0x03, 0x06, 0x09, 0x0c], 22 | 23 | HEADER_LENGTH: 20, 24 | 25 | conn_state: { 26 | 27 | Initial: 0, 28 | Connecting: 1, 29 | Connected: 2, 30 | Binding: 3, 31 | Bound: 4, 32 | Accepting: 5, 33 | Timedout: 6, 34 | Closed: 7, 35 | 36 | }, 37 | 38 | content_type: { 39 | 40 | HelloType: 0, 41 | PingType: 1, 42 | ResultType: 2, 43 | LatencyType: 3, 44 | 45 | // EDGE 46 | Connect: 60783, 47 | StateConnected: 60784, 48 | StateClosed: 60785, 49 | Data: 60786, 50 | Dial: 60787, 51 | DialSuccess: 60788, 52 | DialFailed: 60789, 53 | Bind: 60790, 54 | Unbind: 60791, 55 | StateSessionEnded: 60792, 56 | Probe: 60793, 57 | UpdateBind: 60794, 58 | 59 | }, 60 | 61 | header_id: { 62 | 63 | ConnectionId: 0, 64 | ReplyFor: 1, 65 | ResultSuccess: 2, 66 | HelloListener: 3, 67 | 68 | // Headers in the range 128-255 inclusive will be reflected when creating replies 69 | ReflectedHeaderBitMask: 1 << 7, 70 | MaxReflectedHeader: (1 << 8) - 1, 71 | 72 | ConnId: 1000, 73 | SeqHeader: 1001, 74 | SessionToken: 1002, 75 | PublicKey: 1003, 76 | Cost: 1004, 77 | Precedence: 1005, 78 | TerminatorIdentity: 1006, 79 | TerminatorIdentitySecret: 1007, 80 | CallerId: 1008, 81 | CryptoMethod: 1009, 82 | Flags: 1010, 83 | }, 84 | 85 | header_type: { 86 | IntType: 0, 87 | StringType: 1, 88 | Uint8ArrayType: 2, 89 | } 90 | 91 | 92 | /* 93 | * Channel V2 Wire Format 94 | * 95 | * [ message section ] 96 | * 0 1 2 3 97 | * 4 5 6 7 98 | * 8 9 10 11 99 | * 12 13 14 15 100 | * 16 17 18 19 101 | * 102 | * [ data section ] 103 | * 20 -> (20 + headers-length) 104 | * (20 + headers-length) -> (20 + headers-length + body-length) 105 | */ 106 | 107 | 108 | 109 | } 110 | 111 | module.exports = ZitiEnums; 112 | -------------------------------------------------------------------------------- /src/channel/tls-connection-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | /** 19 | * Default options. 20 | */ 21 | 22 | 23 | module.exports = { 24 | 25 | /** 26 | * See {@link Options.type} 27 | * 28 | */ 29 | type: null, 30 | 31 | /** 32 | * See {@link Options.ctx} 33 | * 34 | */ 35 | ctx: null, 36 | 37 | /** 38 | * See {@link Options.ws} 39 | * 40 | */ 41 | ws: null, 42 | 43 | /** 44 | * See {@link Options.ch} 45 | * 46 | */ 47 | ch: null, 48 | 49 | /** 50 | * See {@link Options.datacb} 51 | * 52 | */ 53 | datacb: null, 54 | 55 | /** 56 | * See {@link Options.data} 57 | * 58 | */ 59 | data: null, 60 | 61 | /** 62 | * See {@link Options.edgeRouterHost} 63 | * 64 | */ 65 | edgeRouterHost: null, 66 | 67 | }; -------------------------------------------------------------------------------- /src/channel/tls-connection.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Module dependencies. 19 | */ 20 | 21 | const Buffer = require('buffer/').Buffer // note: the trailing slash is important! 22 | const flatOptions = require('flat-options'); 23 | const ls = require('../utils/localstorage'); 24 | const defaultOptions = require('./tls-connection-options'); 25 | const utils = require('../utils/utils'); 26 | const zitiConstants = require('../constants'); 27 | const forge = require('node-forge'); 28 | const ab2str = require('arraybuffer-to-string'); 29 | const isUndefined = require('lodash.isundefined'); 30 | const isNull = require('lodash.isnull'); 31 | const { v4: uuidv4 } = require('uuid'); 32 | 33 | forge.options.usePureJavaScript = true; 34 | 35 | 36 | /** 37 | * @typicalname connection 38 | */ 39 | module.exports = class ZitiTLSConnection { 40 | 41 | /** 42 | * 43 | * @param {Options} [options] 44 | */ 45 | constructor(options) { 46 | 47 | this._options = flatOptions(options, defaultOptions); 48 | 49 | this._type = this._options.type; // debugging 50 | 51 | this._ctx = this._options.ctx; 52 | 53 | this._ws = this._options.ws; 54 | 55 | this._ch = this._options.ch; 56 | this._datacb = this._options.datacb; 57 | 58 | this._connected = false; 59 | 60 | this._uuid = uuidv4(); 61 | 62 | } 63 | 64 | 65 | /** 66 | * Populate this TLS Connection object with the keypair from local storage 67 | */ 68 | async pullKeyPair() { 69 | 70 | const self = this; 71 | 72 | return new Promise( async (resolve, reject) => { 73 | 74 | this._clientCertPEM = await ls.getWithExpiry(zitiConstants.get().ZITI_IDENTITY_CERT); 75 | this._clientPrivateKeyPEM = await ls.getWithExpiry(zitiConstants.get().ZITI_IDENTITY_PRIVATE_KEY); 76 | 77 | if ( 78 | isUndefined(this._clientCertPEM) || 79 | isUndefined(this._clientPrivateKeyPEM) || 80 | isNull(this._clientCertPEM) || 81 | isNull(this._clientPrivateKeyPEM) 82 | ) { 83 | return reject( new Error('keypair not present in local storage') ); 84 | } 85 | 86 | return resolve(); 87 | 88 | }); 89 | 90 | } 91 | 92 | 93 | getUUID() { 94 | return this._uuid; 95 | } 96 | 97 | 98 | create() { 99 | 100 | let self = this; 101 | 102 | this._tlsClient = forge.tls.createConnection({ 103 | 104 | // We're always the client 105 | server: false, 106 | 107 | // caStore: self._caStore, /* Array of PEM-formatted certs or a CA store object */ 108 | caStore:forge.pki.createCaStore([]), 109 | 110 | // 111 | sessionCache: {}, 112 | 113 | // These are the cipher suites we support (in order of preference) 114 | cipherSuites: [ 115 | forge.tls.CipherSuites.TLS_RSA_WITH_AES_128_CBC_SHA256, 116 | // forge.tls.CipherSuites.TLS_RSA_WITH_AES_256_CBC_SHA256, 117 | forge.tls.CipherSuites.TLS_RSA_WITH_AES_128_CBC_SHA, 118 | forge.tls.CipherSuites.TLS_RSA_WITH_AES_256_CBC_SHA 119 | ], 120 | 121 | // virtualHost: 'curt-edge-wss-router:3023', 122 | verify: function(connection, verified, depth, certs) { 123 | // skip verification for testing 124 | return true; 125 | 126 | /* 127 | if(depth === 0) { 128 | var cn = certs[0].subject.getField('CN').value; 129 | if(cn !== 'curt-edge-wss-router') { 130 | verified = { 131 | alert: forge.tls.Alert.Description.bad_certificate, 132 | message: 'Certificate common name does not match hostname.' 133 | }; 134 | } 135 | } 136 | return verified; 137 | */ 138 | }, 139 | 140 | connected: function(connection) { 141 | self._ctx.logger.debug('TLS handshake completed successfully'); 142 | 143 | self._connected = true; 144 | 145 | // send message to server 146 | // connection.prepare(forge.util.encodeUtf8('Hi server!')); 147 | /* NOTE: experimental, start heartbeat retransmission timer 148 | myHeartbeatTimer = setInterval(function() { 149 | connection.prepareHeartbeatRequest(forge.util.createBuffer('1234')); 150 | }, 5*60*1000);*/ 151 | }, 152 | 153 | // client-side cert 154 | getCertificate: function(connection, hint) { 155 | self._ctx.logger.trace('getCertificate(): for: %o, [%o]', self._uuid, self._clientCertPEM ); 156 | return self._clientCertPEM; 157 | }, 158 | 159 | // client-side private key 160 | getPrivateKey: function(connection, cert) { 161 | self._ctx.logger.trace('getPrivateKey(): for: %o, [%o]', self._uuid, self._clientPrivateKeyPEM ); 162 | return self._clientPrivateKeyPEM; 163 | }, 164 | 165 | // encrypted data is ready to be sent to the server ---> 166 | tlsDataReady: function(connection) { 167 | let chunk = new Buffer(connection.tlsData.getBytes(), "binary"); 168 | if (chunk.length > 0) { 169 | self._ctx.logger.trace('tlsDataReady: encrypted data is ready to be sent to the server ---> [%o]', chunk); 170 | self._ws.send(chunk); 171 | } 172 | }, 173 | 174 | // clear data from the server is ready <--- 175 | dataReady: function(connection) { 176 | let chunk = new Buffer(connection.data.getBytes(), "binary"); 177 | let ab = chunk.buffer.slice(0, chunk.byteLength); 178 | self._ctx.logger.trace('dataReady: clear data from the server is ready <--- ' ); 179 | self._datacb(self._ch, ab); 180 | }, 181 | 182 | /* NOTE: experimental 183 | heartbeatReceived: function(connection, payload) { 184 | // restart retransmission timer, look at payload 185 | clearInterval(myHeartbeatTimer); 186 | myHeartbeatTimer = setInterval(function() { 187 | connection.prepareHeartbeatRequest(forge.util.createBuffer('1234')); 188 | }, 5*60*1000); 189 | payload.getBytes(); 190 | },*/ 191 | 192 | closed: function(connection) { 193 | self._ctx.logger.debug('disconnected'); 194 | }, 195 | 196 | error: function(connection, error) { 197 | self._ctx.logger.error('uh oh', error); 198 | throw error; 199 | } 200 | }); 201 | } 202 | 203 | 204 | /** 205 | * 206 | */ 207 | handshake() { 208 | this._tlsClient.handshake(); 209 | } 210 | 211 | /** 212 | * 213 | * @param {*} data 214 | */ 215 | isTLSHandshakeComplete() { 216 | return this._connected; 217 | } 218 | 219 | 220 | /** 221 | * 222 | * @param {*} data 223 | */ 224 | process(data) { 225 | this._ctx.logger.trace('process: encrypted data from the server arrived <--- [%o]', data); 226 | let results = this._tlsClient.process(data); 227 | } 228 | 229 | 230 | /** 231 | * 232 | * @param {*} data 233 | */ 234 | prepare(wireData) { 235 | this._ctx.logger.trace('prepare: unencrypted data is ready to be sent to the server ---> [%o]', wireData); 236 | let tlsBinaryString = Buffer.from(wireData).toString('binary') 237 | this._tlsClient.prepare(tlsBinaryString); 238 | } 239 | 240 | } 241 | 242 | 243 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | "use strict"; 19 | 20 | /** 21 | * 22 | */ 23 | const ZITI_CONSTANTS = 24 | { 25 | /** 26 | * The selected JWT to enroll with 27 | */ 28 | 'ZITI_JWT': 'ZITI_JWT', 29 | 30 | /** 31 | * The location of the Controller REST endpoint (as decoded from the JWT) 32 | */ 33 | 'ZITI_CONTROLLER': 'ZITI_CONTROLLER', 34 | 35 | /** 36 | * The location of the Controller WS endpoint (as returned from /protocols) 37 | */ 38 | 'ZITI_CONTROLLER_WS': 'ZITI_CONTROLLER_WS', 39 | 40 | /** 41 | * 42 | */ 43 | 'ZITI_EXPIRY_TIME': 'ZITI_EXPIRY_TIME', 44 | 45 | /** 46 | * The Identity certificate (produced during enrollment) 47 | */ 48 | 'ZITI_IDENTITY_CERT': 'ZITI_IDENTITY_CERT', 49 | 50 | /** 51 | * The Identity public key (generated locally during enrollment) 52 | */ 53 | 'ZITI_IDENTITY_PUBLIC_KEY_FILENAME': 'ZITI_BROWZER_PUBLIC_KEY.pem', 54 | 'ZITI_IDENTITY_PRIVATE_KEY_FILENAME': 'ZITI_BROWZER_PRIVATE_KEY.pem', 55 | 'ZITI_IDENTITY_PUBLIC_KEY_FILE_NOT_FOUND': 'ZITI_IDENTITY_PUBLIC_KEY_FILE_NOT_FOUND', 56 | 'ZITI_IDENTITY_PRIVATE_KEY_FILE_NOT_FOUND': 'ZITI_IDENTITY_PRIVATE_KEY_FILE_NOT_FOUND', 57 | 'ZITI_IDENTITY_KEYPAIR_FOUND': 'ZITI_IDENTITY_KEYPAIR_FOUND', 58 | 'ZITI_IDENTITY_KEYPAIR_OBTAIN_FROM_FS': 'ZITI_IDENTITY_KEYPAIR_OBTAIN_FROM_FS', 59 | 'ZITI_IDENTITY_KEYPAIR_OBTAIN_FROM_IDB': 'ZITI_IDENTITY_KEYPAIR_OBTAIN_FROM_IDB', 60 | 61 | 62 | /** 63 | * The Identity public key (generated locally during enrollment) 64 | */ 65 | 'ZITI_IDENTITY_PUBLIC_KEY': 'ZITI_IDENTITY_PUBLIC_KEY', 66 | 67 | /** 68 | * The Identity private key (generated locally during enrollment) 69 | */ 70 | 'ZITI_IDENTITY_PRIVATE_KEY': 'ZITI_IDENTITY_PRIVATE_KEY', 71 | 72 | /** 73 | * The Identity CA (retrived from Controller during enrollment) 74 | */ 75 | 'ZITI_IDENTITY_CA': 'ZITI_IDENTITY_CA', 76 | 77 | /** 78 | * 79 | */ 80 | 'ZITI_SERVICES': 'ZITI_SERVICES', 81 | 82 | 'ZITI_API_SESSION_TOKEN': 'ZITI_API_SESSION_TOKEN', 83 | 84 | 'ZITI_IDENTITY_USERNAME': 'ZITI_IDENTITY_USERNAME', 85 | 'ZITI_IDENTITY_PASSWORD': 'ZITI_IDENTITY_PASSWORD', 86 | 87 | 'ZITI_NETWORK_SESSIONS': 'ZITI_NETWORK_SESSIONS', 88 | 89 | 90 | /** 91 | * We save Cookies in IndexedDB for the benefit of work happening on/in the service worker. 92 | * This is because normal means of fetching cookies (via document.cookies) will fail because 93 | * there is no 'document' object on that side. 94 | */ 95 | 'ZITI_COOKIES': 'ZITI_COOKIES', 96 | 97 | 98 | /** 99 | * The default timeout in milliseconds for connections and write operations to succeed. 100 | */ 101 | 'ZITI_DEFAULT_TIMEOUT': 10000, 102 | 103 | /** 104 | * The ... 105 | */ 106 | 'ZITI_CLIENT_CERT_PEM': 'ZITI_CLIENT_CERT_PEM', 107 | 108 | /** 109 | * The ... 110 | */ 111 | 'ZITI_CLIENT_PRIVATE_KEY_PEM': 'ZITI_CLIENT_PRIVATE_KEY_PEM', 112 | 113 | }; 114 | 115 | 116 | exports.get = () => { 117 | return ZITI_CONSTANTS; 118 | }; 119 | -------------------------------------------------------------------------------- /src/context/contexttypes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const ContextTypes = { 19 | 20 | ClientType: 1, 21 | ServiceWorkerType: 2, 22 | 23 | } 24 | 25 | module.exports = ContextTypes; 26 | -------------------------------------------------------------------------------- /src/context/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const serviceTypes = require('./servicetypes'); 19 | const contextTypes = require('./contexttypes'); 20 | const LogLevel = require('../logLevels'); 21 | 22 | /** 23 | * Default options. 24 | */ 25 | 26 | 27 | module.exports = { 28 | 29 | /** 30 | * See {@link Options.serviceType} 31 | * 32 | */ 33 | serviceType: serviceTypes.RESTType, 34 | 35 | /** 36 | * See {@link Options.contextType} 37 | * 38 | */ 39 | contextType: contextTypes.ClientType, 40 | 41 | /** 42 | * See {@link Options.logLevel} 43 | * 44 | */ 45 | logLevel: LogLevel.Info, 46 | 47 | }; -------------------------------------------------------------------------------- /src/context/servicetypes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const ServiceTypes = { 19 | 20 | RESTType: 1, 21 | SocketType: 2, 22 | 23 | } 24 | 25 | module.exports = ServiceTypes; 26 | -------------------------------------------------------------------------------- /src/enroll/atob.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 18 | 19 | function InvalidCharacterError(message) { 20 | this.message = message; 21 | } 22 | 23 | InvalidCharacterError.prototype = new Error(); 24 | InvalidCharacterError.prototype.name = "InvalidCharacterError"; 25 | 26 | function polyfill(input) { 27 | var str = String(input).replace(/=+$/, ""); 28 | if (str.length % 4 == 1) { 29 | throw new InvalidCharacterError( 30 | "'atob' failed: The string to be decoded is not correctly encoded." 31 | ); 32 | } 33 | for ( 34 | // initialize result and counters 35 | var bc = 0, bs, buffer, idx = 0, output = ""; 36 | // get next character 37 | (buffer = str.charAt(idx++)); 38 | // character found in table? initialize bit storage and add its ascii value; 39 | ~buffer && 40 | ((bs = bc % 4 ? bs * 64 + buffer : buffer), 41 | // and if not first of each 4 characters, 42 | // convert the first 8 bits to one ascii character 43 | bc++ % 4) ? 44 | (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) : 45 | 0 46 | ) { 47 | // try to find character in table (0-63, not found => -1) 48 | buffer = chars.indexOf(buffer); 49 | } 50 | return output; 51 | } 52 | 53 | let atob = (typeof window !== "undefined" && window.atob && window.atob.bind(window)) || polyfill; 54 | 55 | module.exports = atob; 56 | -------------------------------------------------------------------------------- /src/enroll/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Base64 JavaScript decoder 18 | // Copyright (c) 2008-2020 Lapo Luchini 19 | 20 | // Permission to use, copy, modify, and/or distribute this software for any 21 | // purpose with or without fee is hereby granted, provided that the above 22 | // copyright notice and this permission notice appear in all copies. 23 | // 24 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 25 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 26 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 27 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 28 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 29 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 30 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 31 | 32 | (typeof define != 'undefined' ? define : function (factory) { 'use strict'; 33 | if (typeof module == 'object') module.exports = factory(); 34 | else window.base64 = factory(); 35 | })(function () { 36 | "use strict"; 37 | 38 | var Base64 = {}, 39 | decoder, // populated on first usage 40 | haveU8 = (typeof Uint8Array == 'function'); 41 | 42 | Base64.decode = function (a) { 43 | var isString = (typeof a == 'string'); 44 | var i; 45 | if (decoder === undefined) { 46 | var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", 47 | ignore = "= \f\n\r\t\u00A0\u2028\u2029"; 48 | decoder = []; 49 | for (i = 0; i < 64; ++i) 50 | decoder[b64.charCodeAt(i)] = i; 51 | for (i = 0; i < ignore.length; ++i) 52 | decoder[ignore.charCodeAt(i)] = -1; 53 | // RFC 3548 URL & file safe encoding 54 | decoder['-'.charCodeAt(0)] = decoder['+'.charCodeAt(0)]; 55 | decoder['_'.charCodeAt(0)] = decoder['/'.charCodeAt(0)]; 56 | } 57 | var out = haveU8 ? new Uint8Array(a.length * 3 >> 2) : []; 58 | var bits = 0, char_count = 0, len = 0; 59 | for (i = 0; i < a.length; ++i) { 60 | var c = isString ? a.charCodeAt(i) : a[i]; 61 | if (c == 61) // '='.charCodeAt(0) 62 | break; 63 | c = decoder[c]; 64 | if (c == -1) 65 | continue; 66 | if (c === undefined) 67 | throw 'Illegal character at offset ' + i; 68 | bits |= c; 69 | if (++char_count >= 4) { 70 | out[len++] = (bits >> 16); 71 | out[len++] = (bits >> 8) & 0xFF; 72 | out[len++] = bits & 0xFF; 73 | bits = 0; 74 | char_count = 0; 75 | } else { 76 | bits <<= 6; 77 | } 78 | } 79 | switch (char_count) { 80 | case 1: 81 | throw "Base64 encoding incomplete: at least 2 bits missing"; 82 | case 2: 83 | out[len++] = (bits >> 10); 84 | break; 85 | case 3: 86 | out[len++] = (bits >> 16); 87 | out[len++] = (bits >> 8) & 0xFF; 88 | break; 89 | } 90 | if (haveU8 && out.length > len) // in case it was originally longer because of ignored characters 91 | out = out.subarray(0, len); 92 | return out; 93 | }; 94 | 95 | Base64.pretty = function (str) { 96 | // fix padding 97 | if (str.length % 4 > 0) 98 | str = (str + '===').slice(0, str.length + str.length % 4); 99 | // convert RFC 3548 to standard Base64 100 | str = str.replace(/-/g, '+').replace(/_/g, '/'); 101 | // 80 column width 102 | return str.replace(/(.{80})/g, '$1\n'); 103 | }; 104 | 105 | Base64.re = /-----BEGIN [^-]+-----([A-Za-z0-9+/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+/=\s]+)====/; 106 | Base64.unarmor = function (a) { 107 | var m = Base64.re.exec(a); 108 | if (m) { 109 | if (m[1]) 110 | a = m[1]; 111 | else if (m[2]) 112 | a = m[2]; 113 | else 114 | throw "RegExp out of sync"; 115 | } 116 | return Base64.decode(a); 117 | }; 118 | 119 | return Base64; 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /src/enroll/base64_url_decode.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const atob = require('./atob'); 18 | 19 | 20 | function b64DecodeUnicode(str) { 21 | return decodeURIComponent( 22 | atob(str).replace(/(.)/g, function(m, p) { 23 | var code = p.charCodeAt(0).toString(16).toUpperCase(); 24 | if (code.length < 2) { 25 | code = "0" + code; 26 | } 27 | return "%" + code; 28 | }) 29 | ); 30 | } 31 | 32 | function base_64_decode(str) { 33 | var output = str.replace(/-/g, "+").replace(/_/g, "/"); 34 | switch (output.length % 4) { 35 | case 0: 36 | break; 37 | case 2: 38 | output += "=="; 39 | break; 40 | case 3: 41 | output += "="; 42 | break; 43 | default: 44 | throw "Illegal base64url string!"; 45 | } 46 | 47 | try { 48 | return b64DecodeUnicode(output); 49 | } catch (err) { 50 | return atob(output); 51 | } 52 | } 53 | 54 | module.exports = base_64_decode; 55 | -------------------------------------------------------------------------------- /src/enroll/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | /** 19 | * Default options. 20 | */ 21 | 22 | 23 | module.exports = { 24 | 25 | /** 26 | * See {@link Options.ctx} 27 | * 28 | */ 29 | ctx: null, 30 | 31 | /** 32 | * See {@link Options.logger} 33 | * 34 | */ 35 | logger: null, 36 | 37 | }; -------------------------------------------------------------------------------- /src/http/_http_common.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | 'use strict'; 19 | 20 | const HTTPParser = require('./http-parser'); 21 | const methods = HTTPParser.methods; 22 | 23 | const FreeList = require('./freelist'); 24 | const incoming = require('./_http_incoming'); 25 | const { 26 | IncomingMessage, 27 | readStart, 28 | readStop 29 | } = incoming; 30 | 31 | 32 | const kIncomingMessage = Symbol('IncomingMessage'); 33 | const kOnHeaders = HTTPParser.kOnHeaders | 0; 34 | const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; 35 | const kOnBody = HTTPParser.kOnBody | 0; 36 | const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; 37 | const kOnExecute = HTTPParser.kOnExecute | 0; 38 | const kOnTimeout = HTTPParser.kOnTimeout | 0; 39 | 40 | const MAX_HEADER_PAIRS = 2000; 41 | 42 | // Only called in the slow case where slow means 43 | // that the request headers were either fragmented 44 | // across multiple TCP packets or too large to be 45 | // processed in a single run. This method is also 46 | // called to process trailing HTTP headers. 47 | function parserOnHeaders(headers, url) { 48 | // Once we exceeded headers limit - stop collecting them 49 | if (this.maxHeaderPairs <= 0 || 50 | this._headers.length < this.maxHeaderPairs) { 51 | this._headers = this._headers.concat(headers); 52 | } 53 | this._url += url; 54 | } 55 | 56 | // `headers` and `url` are set only if .onHeaders() has not been called for 57 | // this request. 58 | // `url` is not set for response parsers but that's not applicable here since 59 | // all our parsers are request parsers. 60 | function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, 61 | url, statusCode, statusMessage, upgrade, 62 | shouldKeepAlive) { 63 | const parser = this; 64 | const { socket } = parser; 65 | 66 | if (headers === undefined) { 67 | headers = parser._headers; 68 | parser._headers = []; 69 | } 70 | 71 | if (url === undefined) { 72 | url = parser._url; 73 | parser._url = ''; 74 | } 75 | 76 | // Parser is also used by http client 77 | const ParserIncomingMessage = (socket && socket.server && 78 | socket.server[kIncomingMessage]) || 79 | IncomingMessage; 80 | 81 | const incoming = parser.incoming = new ParserIncomingMessage(socket); 82 | incoming.httpVersionMajor = versionMajor; 83 | incoming.httpVersionMinor = versionMinor; 84 | incoming.httpVersion = `${versionMajor}.${versionMinor}`; 85 | incoming.url = url; 86 | incoming.upgrade = upgrade; 87 | 88 | let n = headers.length; 89 | 90 | // If parser.maxHeaderPairs <= 0 assume that there's no limit. 91 | // if (parser.maxHeaderPairs > 0) 92 | // n = Math.min(n, parser.maxHeaderPairs); 93 | 94 | incoming._addHeaderLines(headers, n); 95 | 96 | if (typeof method === 'number') { 97 | // server only 98 | incoming.method = methods[method]; 99 | } else { 100 | // client only 101 | incoming.statusCode = statusCode; 102 | incoming.statusMessage = statusMessage; 103 | } 104 | 105 | return parser.onIncoming(incoming, shouldKeepAlive); 106 | } 107 | 108 | function parserOnBody(b, start, len) { 109 | const stream = this.incoming; 110 | 111 | // If the stream has already been removed, then drop it. 112 | if (stream === null) 113 | return; 114 | 115 | // Pretend this was the result of a stream._read call. 116 | if (len > 0 && !stream._dumped) { 117 | const slice = b.slice(start, start + len); 118 | const ret = stream.push(slice); 119 | if (!ret) 120 | readStop(this.socket); 121 | } 122 | } 123 | 124 | function parserOnMessageComplete() { 125 | const parser = this; 126 | const stream = parser.incoming; 127 | 128 | if (stream !== null) { 129 | stream.complete = true; 130 | // Emit any trailing headers. 131 | const headers = parser._headers; 132 | if (headers.length) { 133 | stream._addHeaderLines(headers, headers.length); 134 | parser._headers = []; 135 | parser._url = ''; 136 | } 137 | 138 | // For emit end event 139 | stream.push(null); 140 | } 141 | 142 | // Force to read the next incoming message 143 | readStart(parser.socket); 144 | } 145 | 146 | 147 | const parsers = new FreeList('parsers', 1000, function parsersCb() { 148 | const parser = new HTTPParser(); 149 | 150 | cleanParser(parser); 151 | 152 | parser[kOnHeaders] = parserOnHeaders; 153 | parser[kOnHeadersComplete] = parserOnHeadersComplete; 154 | parser[kOnBody] = parserOnBody; 155 | parser[kOnMessageComplete] = parserOnMessageComplete; 156 | 157 | return parser; 158 | }); 159 | 160 | function closeParserInstance(parser) { parser.close(); } 161 | 162 | // Free the parser and also break any links that it 163 | // might have to any other things. 164 | // TODO: All parser data should be attached to a 165 | // single object, so that it can be easily cleaned 166 | // up by doing `parser.data = {}`, which should 167 | // be done in FreeList.free. `parsers.free(parser)` 168 | // should be all that is needed. 169 | function freeParser(parser, req, socket) { 170 | if (parser) { 171 | if (parser._consumed) 172 | parser.unconsume(); 173 | cleanParser(parser); 174 | if (parsers.free(parser) === false) { 175 | // Make sure the parser's stack has unwound before deleting the 176 | // corresponding C++ object through .close(). 177 | setImmediate(closeParserInstance, parser); 178 | } else { 179 | // Since the Parser destructor isn't going to run the destroy() callbacks 180 | // it needs to be triggered manually. 181 | parser.free(); 182 | } 183 | } 184 | if (req) { 185 | req.parser = null; 186 | } 187 | if (socket) { 188 | socket.parser = null; 189 | } 190 | } 191 | 192 | const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; 193 | /** 194 | * Verifies that the given val is a valid HTTP token 195 | * per the rules defined in RFC 7230 196 | * See https://tools.ietf.org/html/rfc7230#section-3.2.6 197 | */ 198 | function checkIsHttpToken(val) { 199 | return tokenRegExp.test(val); 200 | } 201 | 202 | const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; 203 | /** 204 | * True if val contains an invalid field-vchar 205 | * field-value = *( field-content / obs-fold ) 206 | * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] 207 | * field-vchar = VCHAR / obs-text 208 | */ 209 | function checkInvalidHeaderChar(val) { 210 | return headerCharRegex.test(val); 211 | } 212 | 213 | function cleanParser(parser) { 214 | parser._headers = []; 215 | parser._url = ''; 216 | parser.socket = null; 217 | parser.incoming = null; 218 | parser.outgoing = null; 219 | parser.maxHeaderPairs = MAX_HEADER_PAIRS; 220 | parser[kOnExecute] = null; 221 | parser[kOnTimeout] = null; 222 | parser._consumed = false; 223 | parser.onIncoming = null; 224 | } 225 | 226 | function prepareError(err, parser, rawPacket) { 227 | err.rawPacket = rawPacket || parser.getCurrentBuffer(); 228 | if (typeof err.reason === 'string') 229 | err.message = `Parse Error: ${err.reason}`; 230 | } 231 | 232 | 233 | module.exports = { 234 | _checkInvalidHeaderChar: checkInvalidHeaderChar, 235 | _checkIsHttpToken: checkIsHttpToken, 236 | chunkExpression: /(?:^|\W)chunked(?:$|\W)/i, 237 | continueExpression: /(?:^|\W)100-continue(?:$|\W)/i, 238 | CRLF: '\r\n', 239 | freeParser, 240 | methods, 241 | parsers, 242 | kIncomingMessage, 243 | HTTPParser, 244 | prepareError, 245 | }; 246 | -------------------------------------------------------------------------------- /src/http/blob.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const { 19 | Readable, 20 | Writable, 21 | Transform, 22 | Duplex, 23 | pipeline, 24 | finished 25 | } = require('readable-stream'); 26 | 27 | const BUFFER = Symbol('buffer'); 28 | const TYPE = Symbol('type'); 29 | 30 | 31 | /** 32 | * Expose `Blob`. 33 | */ 34 | 35 | module.exports = Blob; 36 | 37 | /** 38 | * Initialize a new `Blob`. 39 | * 40 | * @api public 41 | */ 42 | 43 | function Blob() { 44 | 45 | this[TYPE] = ''; 46 | 47 | const blobParts = arguments[0]; 48 | const options = arguments[1]; 49 | 50 | const buffers = []; 51 | let size = 0; 52 | 53 | if (blobParts) { 54 | const a = blobParts; 55 | const length = Number(a.length); 56 | for (let i = 0; i < length; i++) { 57 | const element = a[i]; 58 | let buffer; 59 | if (element instanceof Buffer) { 60 | buffer = element; 61 | } else if (ArrayBuffer.isView(element)) { 62 | buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); 63 | } else if (element instanceof ArrayBuffer) { 64 | buffer = Buffer.from(element); 65 | } else if (element instanceof Blob) { 66 | buffer = element[BUFFER]; 67 | } else { 68 | buffer = Buffer.from(typeof element === 'string' ? element : String(element)); 69 | } 70 | size += buffer.length; 71 | buffers.push(buffer); 72 | } 73 | } 74 | 75 | this[BUFFER] = Buffer.concat(buffers); 76 | 77 | let type = options && options.type !== undefined && String(options.type).toLowerCase(); 78 | if (type && !/[^\u0020-\u007E]/.test(type)) { 79 | this[TYPE] = type; 80 | } 81 | 82 | var ctx = mixin(this); 83 | 84 | return ctx; 85 | } 86 | 87 | /** 88 | * Mixin the prototype properties. 89 | * 90 | * @param {Object} obj 91 | * @return {Object} 92 | * @api private 93 | */ 94 | 95 | function mixin(obj) { 96 | // for (const key in HttpRequest.prototype) { 97 | // if (Object.prototype.hasOwnProperty.call(HttpRequest.prototype, key)) 98 | // obj[key] = HttpRequest.prototype[key]; 99 | // } 100 | 101 | return obj; 102 | } 103 | 104 | 105 | Blob.prototype.BUFFER = BUFFER; 106 | 107 | Blob.prototype.size = function() { 108 | return this[BUFFER].length; 109 | } 110 | 111 | Blob.prototype.type = function() { 112 | return this[TYPE]; 113 | } 114 | 115 | Blob.prototype.text = async function() { 116 | return Promise.resolve(this[BUFFER].toString()); 117 | } 118 | 119 | Blob.prototype.arrayBuffer = async function() { 120 | const buf = this[BUFFER]; 121 | const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 122 | return Promise.resolve(ab); 123 | } 124 | 125 | Blob.prototype.stream = function() { 126 | // const readable = new ReadableStream(); 127 | const readable = new Duplex(); 128 | readable._read = () => {}; 129 | readable.push(this[BUFFER]); 130 | readable.push(null); 131 | return readable; 132 | } 133 | 134 | Blob.prototype.slice = function() { 135 | const size = this.size; 136 | 137 | const start = arguments[0]; 138 | const end = arguments[1]; 139 | let relativeStart, relativeEnd; 140 | if (start === undefined) { 141 | relativeStart = 0; 142 | } else if (start < 0) { 143 | relativeStart = Math.max(size + start, 0); 144 | } else { 145 | relativeStart = Math.min(start, size); 146 | } 147 | if (end === undefined) { 148 | relativeEnd = size; 149 | } else if (end < 0) { 150 | relativeEnd = Math.max(size + end, 0); 151 | } else { 152 | relativeEnd = Math.min(end, size); 153 | } 154 | const span = Math.max(relativeEnd - relativeStart, 0); 155 | 156 | const buffer = this[BUFFER]; 157 | const slicedBuffer = buffer.slice( 158 | relativeStart, 159 | relativeStart + span 160 | ); 161 | const blob = new Blob([], { type: arguments[2] }); 162 | blob[BUFFER] = slicedBuffer; 163 | return blob; 164 | } 165 | 166 | 167 | -------------------------------------------------------------------------------- /src/http/browser-stdout.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var WritableStream = require('stream').Writable 18 | var inherits = require('util').inherits 19 | 20 | module.exports = BrowserStdout 21 | 22 | inherits(BrowserStdout, WritableStream) 23 | 24 | function BrowserStdout(opts) { 25 | if (!(this instanceof BrowserStdout)) return new BrowserStdout(opts) 26 | opts = opts || {} 27 | WritableStream.call(this, opts) 28 | this.req = opts.req; 29 | } 30 | 31 | BrowserStdout.prototype._write = function(chunk, encoding, cb) { 32 | this.req.write( chunk ); 33 | process.nextTick(cb); 34 | } 35 | -------------------------------------------------------------------------------- /src/http/buffer-util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const { EMPTY_BUFFER } = require('./constants'); 20 | 21 | /** 22 | * Merges an array of buffers into a new buffer. 23 | * 24 | * @param {Buffer[]} list The array of buffers to concat 25 | * @param {Number} totalLength The total length of buffers in the list 26 | * @return {Buffer} The resulting buffer 27 | * @public 28 | */ 29 | function concat(list, totalLength) { 30 | if (list.length === 0) return EMPTY_BUFFER; 31 | if (list.length === 1) return list[0]; 32 | 33 | const target = Buffer.allocUnsafe(totalLength); 34 | let offset = 0; 35 | 36 | for (let i = 0; i < list.length; i++) { 37 | const buf = list[i]; 38 | target.set(buf, offset); 39 | offset += buf.length; 40 | } 41 | 42 | if (offset < totalLength) return target.slice(0, offset); 43 | 44 | return target; 45 | } 46 | 47 | /** 48 | * Masks a buffer using the given mask. 49 | * 50 | * @param {Buffer} source The buffer to mask 51 | * @param {Buffer} mask The mask to use 52 | * @param {Buffer} output The buffer where to store the result 53 | * @param {Number} offset The offset at which to start writing 54 | * @param {Number} length The number of bytes to mask. 55 | * @public 56 | */ 57 | function _mask(source, mask, output, offset, length) { 58 | for (let i = 0; i < length; i++) { 59 | output[offset + i] = source[i] ^ mask[i & 3]; 60 | } 61 | } 62 | 63 | /** 64 | * Unmasks a buffer using the given mask. 65 | * 66 | * @param {Buffer} buffer The buffer to unmask 67 | * @param {Buffer} mask The mask to use 68 | * @public 69 | */ 70 | function _unmask(buffer, mask) { 71 | // Required until https://github.com/nodejs/node/issues/9006 is resolved. 72 | const length = buffer.length; 73 | for (let i = 0; i < length; i++) { 74 | buffer[i] ^= mask[i & 3]; 75 | } 76 | } 77 | 78 | /** 79 | * Converts a buffer to an `ArrayBuffer`. 80 | * 81 | * @param {Buffer} buf The buffer to convert 82 | * @return {ArrayBuffer} Converted buffer 83 | * @public 84 | */ 85 | function toArrayBuffer(buf) { 86 | if (buf.byteLength === buf.buffer.byteLength) { 87 | return buf.buffer; 88 | } 89 | 90 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 91 | } 92 | 93 | /** 94 | * Converts `data` to a `Buffer`. 95 | * 96 | * @param {*} data The data to convert 97 | * @return {Buffer} The buffer 98 | * @throws {TypeError} 99 | * @public 100 | */ 101 | function toBuffer(data) { 102 | toBuffer.readOnly = true; 103 | 104 | if (Buffer.isBuffer(data)) return data; 105 | 106 | let buf; 107 | 108 | if (data instanceof ArrayBuffer) { 109 | buf = Buffer.from(data); 110 | } else if (ArrayBuffer.isView(data)) { 111 | buf = viewToBuffer(data); 112 | } else { 113 | buf = Buffer.from(data); 114 | toBuffer.readOnly = false; 115 | } 116 | 117 | return buf; 118 | } 119 | 120 | /** 121 | * Converts an `ArrayBuffer` view into a buffer. 122 | * 123 | * @param {(DataView|TypedArray)} view The view to convert 124 | * @return {Buffer} Converted view 125 | * @private 126 | */ 127 | function viewToBuffer(view) { 128 | const buf = Buffer.from(view.buffer); 129 | 130 | if (view.byteLength !== view.buffer.byteLength) { 131 | return buf.slice(view.byteOffset, view.byteOffset + view.byteLength); 132 | } 133 | 134 | return buf; 135 | } 136 | 137 | try { 138 | const bufferUtil = require('bufferutil'); 139 | const bu = bufferUtil.BufferUtil || bufferUtil; 140 | 141 | module.exports = { 142 | concat, 143 | mask(source, mask, output, offset, length) { 144 | if (length < 48) _mask(source, mask, output, offset, length); 145 | else bu.mask(source, mask, output, offset, length); 146 | }, 147 | toArrayBuffer, 148 | toBuffer, 149 | unmask(buffer, mask) { 150 | if (buffer.length < 32) _unmask(buffer, mask); 151 | else bu.unmask(buffer, mask); 152 | } 153 | }; 154 | } catch (e) /* istanbul ignore next */ { 155 | module.exports = { 156 | concat, 157 | mask: _mask, 158 | toArrayBuffer, 159 | toBuffer, 160 | unmask: _unmask 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /src/http/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | module.exports = { 20 | BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], 21 | 22 | /** 23 | * This GUID is defined by the Websocket protocol (https://tools.ietf.org/html/rfc6455) 24 | */ 25 | GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 26 | 27 | kStatusCode: Symbol('status-code'), 28 | 29 | kWebSocket: Symbol('websocket'), 30 | 31 | EMPTY_BUFFER: Buffer.alloc(0), 32 | 33 | NOOP: () => {} 34 | }; 35 | -------------------------------------------------------------------------------- /src/http/event-target.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Class representing an event. 21 | * 22 | * @private 23 | */ 24 | class Event { 25 | /** 26 | * Create a new `Event`. 27 | * 28 | * @param {String} type The name of the event 29 | * @param {Object} target A reference to the target to which the event was dispatched 30 | */ 31 | constructor(type, target) { 32 | this.target = target; 33 | this.type = type; 34 | } 35 | } 36 | 37 | /** 38 | * Class representing a message event. 39 | * 40 | * @extends Event 41 | * @private 42 | */ 43 | class MessageEvent extends Event { 44 | /** 45 | * Create a new `MessageEvent`. 46 | * 47 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data 48 | * @param {WebSocket} target A reference to the target to which the event was dispatched 49 | */ 50 | constructor(data, target) { 51 | super('message', target); 52 | 53 | this.data = data; 54 | } 55 | } 56 | 57 | /** 58 | * Class representing a close event. 59 | * 60 | * @extends Event 61 | * @private 62 | */ 63 | class CloseEvent extends Event { 64 | /** 65 | * Create a new `CloseEvent`. 66 | * 67 | * @param {Number} code The status code explaining why the connection is being closed 68 | * @param {String} reason A human-readable string explaining why the connection is closing 69 | * @param {WebSocket} target A reference to the target to which the event was dispatched 70 | */ 71 | constructor(code, reason, target) { 72 | super('close', target); 73 | 74 | this.wasClean = target._closeFrameReceived && target._closeFrameSent; 75 | this.reason = reason; 76 | this.code = code; 77 | } 78 | } 79 | 80 | /** 81 | * Class representing an open event. 82 | * 83 | * @extends Event 84 | * @private 85 | */ 86 | class OpenEvent extends Event { 87 | /** 88 | * Create a new `OpenEvent`. 89 | * 90 | * @param {WebSocket} target A reference to the target to which the event was dispatched 91 | */ 92 | constructor(target) { 93 | super('open', target); 94 | } 95 | } 96 | 97 | /** 98 | * Class representing an error event. 99 | * 100 | * @extends Event 101 | * @private 102 | */ 103 | class ErrorEvent extends Event { 104 | /** 105 | * Create a new `ErrorEvent`. 106 | * 107 | * @param {Object} error The error that generated this event 108 | * @param {WebSocket} target A reference to the target to which the event was dispatched 109 | */ 110 | constructor(error, target) { 111 | super('error', target); 112 | 113 | this.message = error.message; 114 | this.error = error; 115 | } 116 | } 117 | 118 | /** 119 | * This provides methods for emulating the `EventTarget` interface. It's not 120 | * meant to be used directly. 121 | * 122 | * @mixin 123 | */ 124 | const EventTarget = { 125 | /** 126 | * Register an event listener. 127 | * 128 | * @param {String} method A string representing the event type to listen for 129 | * @param {Function} listener The listener to add 130 | * @public 131 | */ 132 | addEventListener(method, listener) { 133 | if (typeof listener !== 'function') return; 134 | 135 | function onMessage(data) { 136 | listener.call(this, new MessageEvent(data, this)); 137 | } 138 | 139 | function onClose(code, message) { 140 | listener.call(this, new CloseEvent(code, message, this)); 141 | } 142 | 143 | function onError(error) { 144 | listener.call(this, new ErrorEvent(error, this)); 145 | } 146 | 147 | function onOpen() { 148 | listener.call(this, new OpenEvent(this)); 149 | } 150 | 151 | if (method === 'message') { 152 | onMessage._listener = listener; 153 | this.on(method, onMessage); 154 | } else if (method === 'close') { 155 | onClose._listener = listener; 156 | this.on(method, onClose); 157 | } else if (method === 'error') { 158 | onError._listener = listener; 159 | this.on(method, onError); 160 | } else if (method === 'open') { 161 | onOpen._listener = listener; 162 | this.on(method, onOpen); 163 | } else { 164 | this.on(method, listener); 165 | } 166 | }, 167 | 168 | /** 169 | * Remove an event listener. 170 | * 171 | * @param {String} method A string representing the event type to remove 172 | * @param {Function} listener The listener to remove 173 | * @public 174 | */ 175 | removeEventListener(method, listener) { 176 | const listeners = this.listeners(method); 177 | 178 | for (let i = 0; i < listeners.length; i++) { 179 | if (listeners[i] === listener || listeners[i]._listener === listener) { 180 | this.removeListener(method, listeners[i]); 181 | } 182 | } 183 | } 184 | }; 185 | 186 | module.exports = EventTarget; 187 | -------------------------------------------------------------------------------- /src/http/file-reader-stream.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var from2 = require('from2') 18 | var toBuffer = require('typedarray-to-buffer') 19 | 20 | module.exports = function (file, options) { 21 | options = options || {} 22 | var offset = options.offset || 0 23 | var chunkSize = options.chunkSize || 1024 * 4 24 | var fileReader = new FileReader(file) 25 | 26 | var from = from2(function (size, cb) { 27 | if (offset >= file.size) return cb(null, null) 28 | fileReader.onloadend = function loaded (event) { 29 | var data = event.target.result 30 | if (data instanceof ArrayBuffer) data = toBuffer(new Uint8Array(event.target.result)) 31 | cb(null, data) 32 | } 33 | var end = offset + chunkSize 34 | var slice = file.slice(offset, end) 35 | fileReader.readAsArrayBuffer(slice) 36 | offset = end 37 | }) 38 | 39 | from.name = file.name 40 | from.size = file.size 41 | from.type = file.type 42 | from.lastModified = file.lastModified 43 | 44 | fileReader.onerror = function (err) { 45 | from.destroy(err) 46 | } 47 | 48 | return from 49 | } 50 | -------------------------------------------------------------------------------- /src/http/file-reader.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | /** 19 | * Expose `ZitiFileReader`. 20 | */ 21 | 22 | module.exports = ZitiFileReader; 23 | 24 | 25 | function ZitiFileReader( file ) { 26 | this._file = file; 27 | this._fileSize = this._file.size; 28 | this._offset = 0; 29 | this._chunkSize = 65536; 30 | this._dataBuffer = new Buffer.alloc( 0 ); 31 | } 32 | 33 | 34 | /** 35 | * 36 | * @returns Promise 37 | */ 38 | ZitiFileReader.prototype.getBuffer = function () { 39 | 40 | let self = this; 41 | 42 | return new Promise(function(resolve, reject) { 43 | 44 | var chunkReaderBlock = null; 45 | 46 | var readEventHandler = function( evt ) { 47 | 48 | if (evt.target.error === null) { 49 | 50 | self._offset += evt.target.result.byteLength; 51 | let buf = new Buffer.from( evt.target.result ); 52 | self._dataBuffer = Buffer.concat( [self._dataBuffer, buf]); 53 | 54 | } else { 55 | 56 | console.error("Read error: " + evt.target.error); 57 | return reject(); 58 | 59 | } 60 | 61 | if (self._offset >= self._fileSize) { 62 | return resolve( self._dataBuffer ); 63 | } 64 | 65 | // off to the next chunk 66 | chunkReaderBlock(self._offset, self._chunkSize, self._file); 67 | } 68 | 69 | chunkReaderBlock = function(_offset, length, _file) { 70 | var r = new FileReader(); 71 | var blob = _file.slice(_offset, length + _offset); 72 | r.onload = readEventHandler; 73 | r.readAsArrayBuffer(blob); 74 | } 75 | 76 | // now let's start the read with the first block 77 | chunkReaderBlock(self._offset, self._chunkSize, self._file); 78 | 79 | }); 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/http/freelist.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | class FreeList { 20 | constructor(name, max, ctor) { 21 | this.name = name; 22 | this.ctor = ctor; 23 | this.max = max; 24 | this.list = []; 25 | } 26 | 27 | alloc() { 28 | return this.list.length > 0 ? 29 | this.list.pop() : 30 | Reflect.apply(this.ctor, this, arguments); 31 | } 32 | 33 | free(obj) { 34 | if (this.list.length < this.max) { 35 | this.list.push(obj); 36 | return true; 37 | } 38 | return false; 39 | } 40 | } 41 | 42 | module.exports = FreeList; 43 | -------------------------------------------------------------------------------- /src/http/headers.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const isUndefined = require('lodash.isundefined'); 18 | 19 | const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; 20 | const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; 21 | 22 | function validateName(name) { 23 | name = `${name}`; 24 | if (invalidTokenRegex.test(name) || name === '') { 25 | throw new TypeError(`${name} is not a legal HTTP header name`); 26 | } 27 | } 28 | 29 | function validateValue(value) { 30 | value = `${value}`; 31 | if (invalidHeaderCharRegex.test(value)) { 32 | throw new TypeError(`${value} is not a legal HTTP header value`); 33 | } 34 | } 35 | 36 | /** 37 | * Find the key in the map object given a header name. 38 | * 39 | * Returns undefined if not found. 40 | * 41 | * @param String name Header name 42 | * @return String|Undefined 43 | */ 44 | function find(map, name) { 45 | name = name.toLowerCase(); 46 | for (const key in map) { 47 | if (key.toLowerCase() === name) { 48 | return key; 49 | } 50 | } 51 | return undefined; 52 | } 53 | 54 | 55 | const MAP = Symbol('map'); 56 | 57 | 58 | function getHeaders(headers, kind = 'key+value') { 59 | const keys = Object.keys(headers[MAP]).sort(); 60 | return keys.map( 61 | kind === 'key' ? 62 | k => k.toLowerCase() : 63 | kind === 'value' ? 64 | k => headers[MAP][k].join(', ') : 65 | k => [k.toLowerCase(), headers[MAP][k].join(', ')] 66 | ); 67 | } 68 | 69 | const INTERNAL = Symbol('internal'); 70 | 71 | function createHeadersIterator(target, kind) { 72 | const iterator = Object.create(HeadersIteratorPrototype); 73 | iterator[INTERNAL] = { 74 | target, 75 | kind, 76 | index: 0 77 | }; 78 | return iterator; 79 | } 80 | 81 | const HeadersIteratorPrototype = Object.setPrototypeOf({ 82 | next() { 83 | // istanbul ignore if 84 | if (!this || 85 | Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { 86 | throw new TypeError('Value of `this` is not a HeadersIterator'); 87 | } 88 | 89 | const { 90 | target, 91 | kind, 92 | index 93 | } = this[INTERNAL]; 94 | const values = getHeaders(target, kind); 95 | const len = values.length; 96 | if (index >= len) { 97 | return { 98 | value: undefined, 99 | done: true 100 | }; 101 | } 102 | 103 | this[INTERNAL].index = index + 1; 104 | 105 | return { 106 | value: values[index], 107 | done: false 108 | }; 109 | } 110 | }, Object.getPrototypeOf( 111 | Object.getPrototypeOf([][Symbol.iterator]()) 112 | )); 113 | 114 | Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { 115 | value: 'HeadersIterator', 116 | writable: false, 117 | enumerable: false, 118 | configurable: true 119 | }); 120 | 121 | 122 | 123 | /** 124 | * Expose `HttpHeaders`. 125 | */ 126 | 127 | module.exports = HttpHeaders; 128 | 129 | /** 130 | * Initialize a new `HttpHeaders`. 131 | * 132 | * @param Object headers Response headers 133 | * @api public 134 | */ 135 | 136 | function HttpHeaders(init = undefined) { 137 | 138 | this[MAP] = Object.create(null); 139 | 140 | if (init instanceof HttpHeaders) { 141 | const rawHeaders = init.raw(); 142 | const headerNames = Object.keys(rawHeaders); 143 | 144 | for (const headerName of headerNames) { 145 | for (const value of rawHeaders[headerName]) { 146 | this.append(headerName, value); 147 | } 148 | } 149 | 150 | return; 151 | } 152 | 153 | // We don't worry about converting prop to ByteString here as append() 154 | // will handle it. 155 | if (init === null) { 156 | // no op 157 | } else if (typeof init === 'object') { 158 | const method = init[Symbol.iterator]; 159 | if (!isUndefined(method) && method !== null) { 160 | if (typeof method !== 'function') { 161 | throw new TypeError('Header pairs must be iterable'); 162 | } 163 | 164 | // sequence> 165 | // Note: per spec we have to first exhaust the lists then process them 166 | const pairs = []; 167 | for (const pair of init) { 168 | if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { 169 | throw new TypeError('Each header pair must be iterable'); 170 | } 171 | pairs.push(Array.from(pair)); 172 | } 173 | 174 | for (const pair of pairs) { 175 | if (pair.length !== 2) { 176 | throw new TypeError('Each header pair must be a name/value tuple'); 177 | } 178 | this.append(pair[0], pair[1]); 179 | } 180 | } else { 181 | // record 182 | for (const key of Object.keys(init)) { 183 | const value = init[key]; 184 | this.append(key, value); 185 | } 186 | } 187 | } else { 188 | throw new TypeError('Provided initializer must be an object'); 189 | } 190 | 191 | var ctx = mixin(this); 192 | 193 | return ctx; 194 | } 195 | 196 | /** 197 | * Mixin the prototype properties. 198 | * 199 | * @param {Object} obj 200 | * @return {Object} 201 | * @api private 202 | */ 203 | 204 | function mixin(obj) { 205 | for (const key in HttpHeaders.prototype) { 206 | if (Object.prototype.hasOwnProperty.call(HttpHeaders.prototype, key)) 207 | obj[key] = HttpHeaders.prototype[key]; 208 | } 209 | 210 | return obj; 211 | } 212 | 213 | 214 | /** 215 | * Return combined header value given name 216 | * 217 | * @param String name Header name 218 | * @return Mixed 219 | */ 220 | HttpHeaders.prototype.get = function(name) { 221 | name = `${name}`; 222 | validateName(name); 223 | const key = find(this[MAP], name); 224 | if (key === undefined) { 225 | return null; 226 | } 227 | 228 | return this[MAP][key].join(', '); 229 | } 230 | 231 | 232 | /** 233 | * Iterate over all headers 234 | * 235 | * @param Function callback Executed for each item with parameters (value, name, thisArg) 236 | * @param Boolean thisArg `this` context for callback function 237 | * @return Void 238 | */ 239 | HttpHeaders.prototype.forEach = function(callback, thisArg = undefined) { 240 | let pairs = getHeaders(this); 241 | let i = 0; 242 | while (i < pairs.length) { 243 | const [name, value] = pairs[i]; 244 | callback.call(thisArg, value, name, this); 245 | pairs = getHeaders(this); 246 | i++; 247 | } 248 | } 249 | 250 | 251 | /** 252 | * Overwrite header values given name 253 | * 254 | * @param String name Header name 255 | * @param String value Header value 256 | * @return Void 257 | */ 258 | HttpHeaders.prototype.set = function(name, value) { 259 | name = `${name}`; 260 | value = `${value}`; 261 | validateName(name); 262 | validateValue(value); 263 | const key = find(this[MAP], name); 264 | this[MAP][key !== undefined ? key : name] = [value]; 265 | } 266 | 267 | 268 | /** 269 | * Append a value onto existing header 270 | * 271 | * @param String name Header name 272 | * @param String value Header value 273 | * @return Void 274 | */ 275 | HttpHeaders.prototype.append = function(name, value) { 276 | name = `${name}`; 277 | // name = name.toLowerCase(); 278 | value = `${value}`; 279 | validateName(name); 280 | validateValue(value); 281 | const key = find(this[MAP], name); 282 | if (key !== undefined) { 283 | this[MAP][key].push(value); 284 | } else { 285 | this[MAP][name] = [value]; 286 | } 287 | } 288 | 289 | 290 | /** 291 | * Check for header name existence 292 | * 293 | * @param String name Header name 294 | * @return Boolean 295 | */ 296 | HttpHeaders.prototype.has = function(name) { 297 | name = `${name}`; 298 | validateName(name); 299 | let value = find(this[MAP], name); 300 | let result = (value !== undefined); 301 | return result; 302 | } 303 | 304 | 305 | /** 306 | * Delete all header values given name 307 | * 308 | * @param String name Header name 309 | * @return Void 310 | */ 311 | HttpHeaders.prototype.delete = function(name) { 312 | name = `${name}`; 313 | validateName(name); 314 | const key = find(this[MAP], name); 315 | if (key !== undefined) { 316 | delete this[MAP][key]; 317 | } 318 | } 319 | 320 | 321 | /** 322 | * Return raw headers (non-spec api) 323 | * 324 | * @return Object 325 | */ 326 | HttpHeaders.prototype.raw = function() { 327 | return this[MAP]; 328 | } 329 | 330 | 331 | /** 332 | * Get an iterator on keys. 333 | * 334 | * @return Iterator 335 | */ 336 | HttpHeaders.prototype.keys = function() { 337 | return createHeadersIterator(this, 'key'); 338 | } 339 | 340 | 341 | /** 342 | * Get an iterator on values. 343 | * 344 | * @return Iterator 345 | */ 346 | HttpHeaders.prototype.values = function() { 347 | return createHeadersIterator(this, 'value'); 348 | } 349 | 350 | 351 | /** 352 | * Get an iterator on entries. 353 | * 354 | * This is the default iterator of the HttpHeaders object. 355 | * 356 | * @return Iterator 357 | */ 358 | HttpHeaders.prototype.entries = function() { 359 | return createHeadersIterator(this, 'key+value'); 360 | } 361 | 362 | 363 | 364 | /** 365 | * Create a HttpHeaders object from an object of headers, ignoring those that do 366 | * not conform to HTTP grammar productions. 367 | * 368 | * @param Object obj Object of headers 369 | * @return HttpHeaders 370 | */ 371 | HttpHeaders.prototype.createHeadersLenient = function(obj) { 372 | const headers = new HttpHeaders(); 373 | for (const name of Object.keys(obj)) { 374 | if (invalidTokenRegex.test(name)) { 375 | continue; 376 | } 377 | if (Array.isArray(obj[name])) { 378 | for (const val of obj[name]) { 379 | if (invalidHeaderCharRegex.test(val)) { 380 | continue; 381 | } 382 | if (headers[MAP][name] === undefined) { 383 | headers[MAP][name] = [val]; 384 | } else { 385 | headers[MAP][name].push(val); 386 | } 387 | } 388 | } else if (!invalidHeaderCharRegex.test(obj[name])) { 389 | headers[MAP][name] = [obj[name]]; 390 | } 391 | } 392 | return headers; 393 | } 394 | 395 | HttpHeaders.prototype.getAllHeaders = function() { 396 | let pairs = getHeaders(this); 397 | return pairs; 398 | } 399 | -------------------------------------------------------------------------------- /src/http/http.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | 'use strict'; 19 | 20 | const { ClientRequest } = require('./_http_client'); 21 | const HTTPParser = require('./http-parser'); 22 | const methods = HTTPParser.methods; 23 | 24 | const { IncomingMessage } = require('./_http_incoming'); 25 | const { 26 | validateHeaderName, 27 | validateHeaderValue, 28 | OutgoingMessage 29 | } = require('./_http_outgoing'); 30 | 31 | function request(url, options, cb) { 32 | return new ClientRequest(url, options, cb); 33 | } 34 | 35 | function get(url, options, cb) { 36 | const req = request(url, options, cb); 37 | req.end(); 38 | return req; 39 | } 40 | 41 | module.exports = { 42 | METHODS: methods.slice().sort(), 43 | ClientRequest, 44 | IncomingMessage, 45 | OutgoingMessage, 46 | validateHeaderName, 47 | validateHeaderValue, 48 | get, 49 | request 50 | }; 51 | -------------------------------------------------------------------------------- /src/http/internal/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const isWindows = process.platform === 'win32'; 20 | 21 | module.exports = { 22 | // Alphabet chars. 23 | CHAR_UPPERCASE_A: 65, /* A */ 24 | CHAR_LOWERCASE_A: 97, /* a */ 25 | CHAR_UPPERCASE_Z: 90, /* Z */ 26 | CHAR_LOWERCASE_Z: 122, /* z */ 27 | CHAR_UPPERCASE_C: 67, /* C */ 28 | CHAR_LOWERCASE_B: 98, /* b */ 29 | CHAR_LOWERCASE_E: 101, /* e */ 30 | CHAR_LOWERCASE_N: 110, /* n */ 31 | 32 | // Non-alphabetic chars. 33 | CHAR_DOT: 46, /* . */ 34 | CHAR_FORWARD_SLASH: 47, /* / */ 35 | CHAR_BACKWARD_SLASH: 92, /* \ */ 36 | CHAR_VERTICAL_LINE: 124, /* | */ 37 | CHAR_COLON: 58, /* : */ 38 | CHAR_QUESTION_MARK: 63, /* ? */ 39 | CHAR_UNDERSCORE: 95, /* _ */ 40 | CHAR_LINE_FEED: 10, /* \n */ 41 | CHAR_CARRIAGE_RETURN: 13, /* \r */ 42 | CHAR_TAB: 9, /* \t */ 43 | CHAR_FORM_FEED: 12, /* \f */ 44 | CHAR_EXCLAMATION_MARK: 33, /* ! */ 45 | CHAR_HASH: 35, /* # */ 46 | CHAR_SPACE: 32, /* */ 47 | CHAR_NO_BREAK_SPACE: 160, /* \u00A0 */ 48 | CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279, /* \uFEFF */ 49 | CHAR_LEFT_SQUARE_BRACKET: 91, /* [ */ 50 | CHAR_RIGHT_SQUARE_BRACKET: 93, /* ] */ 51 | CHAR_LEFT_ANGLE_BRACKET: 60, /* < */ 52 | CHAR_RIGHT_ANGLE_BRACKET: 62, /* > */ 53 | CHAR_LEFT_CURLY_BRACKET: 123, /* { */ 54 | CHAR_RIGHT_CURLY_BRACKET: 125, /* } */ 55 | CHAR_HYPHEN_MINUS: 45, /* - */ 56 | CHAR_PLUS: 43, /* + */ 57 | CHAR_DOUBLE_QUOTE: 34, /* " */ 58 | CHAR_SINGLE_QUOTE: 39, /* ' */ 59 | CHAR_PERCENT: 37, /* % */ 60 | CHAR_SEMICOLON: 59, /* ; */ 61 | CHAR_CIRCUMFLEX_ACCENT: 94, /* ^ */ 62 | CHAR_GRAVE_ACCENT: 96, /* ` */ 63 | CHAR_AT: 64, /* @ */ 64 | CHAR_AMPERSAND: 38, /* & */ 65 | CHAR_EQUAL: 61, /* = */ 66 | 67 | // Digits 68 | CHAR_0: 48, /* 0 */ 69 | CHAR_9: 57, /* 9 */ 70 | 71 | EOL: isWindows ? '\r\n' : '\n' 72 | }; 73 | -------------------------------------------------------------------------------- /src/http/internal/http.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | 20 | let nowCache; 21 | let utcCache; 22 | 23 | function nowDate() { 24 | if (!nowCache) cache(); 25 | return nowCache; 26 | } 27 | 28 | function utcDate() { 29 | if (!utcCache) cache(); 30 | return utcCache; 31 | } 32 | 33 | function cache() { 34 | const d = new Date(); 35 | nowCache = d.valueOf(); 36 | utcCache = d.toUTCString(); 37 | } 38 | 39 | function resetCache() { 40 | nowCache = undefined; 41 | utcCache = undefined; 42 | } 43 | 44 | // class HttpRequestTiming extends PerformanceEntry { 45 | // constructor(statistics) { 46 | // super(); 47 | // this.name = 'HttpRequest'; 48 | // this.entryType = 'http'; 49 | // const startTime = statistics.startTime; 50 | // const diff = process.hrtime(startTime); 51 | // this.duration = diff[0] * 1000 + diff[1] / 1e6; 52 | // this.startTime = startTime[0] * 1000 + startTime[1] / 1e6; 53 | // } 54 | // } 55 | 56 | function emitStatistics(statistics) { 57 | // notify('http', new HttpRequestTiming(statistics)); 58 | } 59 | 60 | module.exports = { 61 | kOutHeaders: Symbol('kOutHeaders'), 62 | kNeedDrain: Symbol('kNeedDrain'), 63 | nowDate, 64 | utcDate, 65 | emitStatistics 66 | }; 67 | -------------------------------------------------------------------------------- /src/http/internal/querystring.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const { ERR_INVALID_URI } = require('./errors').codes; 20 | 21 | const hexTable = new Array(256); 22 | for (let i = 0; i < 256; ++i) 23 | hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); 24 | 25 | const isHexTable = [ 26 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 27 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 28 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47 29 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 30 | 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79 31 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95 32 | 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111 33 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127 34 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ... 35 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // ... 256 42 | ]; 43 | 44 | function encodeStr(str, noEscapeTable, hexTable) { 45 | const len = str.length; 46 | if (len === 0) 47 | return ''; 48 | 49 | let out = ''; 50 | let lastPos = 0; 51 | let i = 0; 52 | 53 | outer: 54 | for (; i < len; i++) { 55 | let c = str.charCodeAt(i); 56 | 57 | // ASCII 58 | while (c < 0x80) { 59 | if (noEscapeTable[c] !== 1) { 60 | if (lastPos < i) 61 | out += str.slice(lastPos, i); 62 | lastPos = i + 1; 63 | out += hexTable[c]; 64 | } 65 | 66 | if (++i === len) 67 | break outer; 68 | 69 | c = str.charCodeAt(i); 70 | } 71 | 72 | if (lastPos < i) 73 | out += str.slice(lastPos, i); 74 | 75 | // Multi-byte characters ... 76 | if (c < 0x800) { 77 | lastPos = i + 1; 78 | out += hexTable[0xC0 | (c >> 6)] + 79 | hexTable[0x80 | (c & 0x3F)]; 80 | continue; 81 | } 82 | if (c < 0xD800 || c >= 0xE000) { 83 | lastPos = i + 1; 84 | out += hexTable[0xE0 | (c >> 12)] + 85 | hexTable[0x80 | ((c >> 6) & 0x3F)] + 86 | hexTable[0x80 | (c & 0x3F)]; 87 | continue; 88 | } 89 | // Surrogate pair 90 | ++i; 91 | 92 | // This branch should never happen because all URLSearchParams entries 93 | // should already be converted to USVString. But, included for 94 | // completion's sake anyway. 95 | if (i >= len) 96 | throw new ERR_INVALID_URI(); 97 | 98 | const c2 = str.charCodeAt(i) & 0x3FF; 99 | 100 | lastPos = i + 1; 101 | c = 0x10000 + (((c & 0x3FF) << 10) | c2); 102 | out += hexTable[0xF0 | (c >> 18)] + 103 | hexTable[0x80 | ((c >> 12) & 0x3F)] + 104 | hexTable[0x80 | ((c >> 6) & 0x3F)] + 105 | hexTable[0x80 | (c & 0x3F)]; 106 | } 107 | if (lastPos === 0) 108 | return str; 109 | if (lastPos < len) 110 | return out + str.slice(lastPos); 111 | return out; 112 | } 113 | 114 | module.exports = { 115 | encodeStr, 116 | hexTable, 117 | isHexTable 118 | }; 119 | -------------------------------------------------------------------------------- /src/http/internal/util/types.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | 20 | function uncurryThis(func) { 21 | return function () { 22 | return Function.call.apply(func, arguments); 23 | } 24 | } 25 | 26 | 27 | const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype); 28 | 29 | const TypedArrayProto_toStringTag = 30 | uncurryThis( 31 | Object.getOwnPropertyDescriptor(TypedArrayPrototype, 32 | Symbol.toStringTag).get); 33 | 34 | function isTypedArray(value) { 35 | return TypedArrayProto_toStringTag(value) !== undefined; 36 | } 37 | 38 | function isUint8Array(value) { 39 | return TypedArrayProto_toStringTag(value) === 'Uint8Array'; 40 | } 41 | 42 | function isUint8ClampedArray(value) { 43 | return TypedArrayProto_toStringTag(value) === 'Uint8ClampedArray'; 44 | } 45 | 46 | function isUint16Array(value) { 47 | return TypedArrayProto_toStringTag(value) === 'Uint16Array'; 48 | } 49 | 50 | function isUint32Array(value) { 51 | return TypedArrayProto_toStringTag(value) === 'Uint32Array'; 52 | } 53 | 54 | function isInt8Array(value) { 55 | return TypedArrayProto_toStringTag(value) === 'Int8Array'; 56 | } 57 | 58 | function isInt16Array(value) { 59 | return TypedArrayProto_toStringTag(value) === 'Int16Array'; 60 | } 61 | 62 | function isInt32Array(value) { 63 | return TypedArrayProto_toStringTag(value) === 'Int32Array'; 64 | } 65 | 66 | function isFloat32Array(value) { 67 | return TypedArrayProto_toStringTag(value) === 'Float32Array'; 68 | } 69 | 70 | function isFloat64Array(value) { 71 | return TypedArrayProto_toStringTag(value) === 'Float64Array'; 72 | } 73 | 74 | function isBigInt64Array(value) { 75 | return TypedArrayProto_toStringTag(value) === 'BigInt64Array'; 76 | } 77 | 78 | function isBigUint64Array(value) { 79 | return TypedArrayProto_toStringTag(value) === 'BigUint64Array'; 80 | } 81 | 82 | module.exports = { 83 | // ...internalBinding('types'), 84 | isArrayBufferView: ArrayBuffer.isView, 85 | isTypedArray, 86 | isUint8Array, 87 | isUint8ClampedArray, 88 | isUint16Array, 89 | isUint32Array, 90 | isInt8Array, 91 | isInt16Array, 92 | isInt32Array, 93 | isFloat32Array, 94 | isFloat64Array, 95 | isBigInt64Array, 96 | isBigUint64Array 97 | }; 98 | -------------------------------------------------------------------------------- /src/http/limiter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const kDone = Symbol('kDone'); 20 | const kRun = Symbol('kRun'); 21 | 22 | /** 23 | * A very simple job queue with adjustable concurrency. Adapted from 24 | * https://github.com/STRML/async-limiter 25 | */ 26 | class Limiter { 27 | /** 28 | * Creates a new `Limiter`. 29 | * 30 | * @param {Number} concurrency The maximum number of jobs allowed to run 31 | * concurrently 32 | */ 33 | constructor(concurrency) { 34 | this[kDone] = () => { 35 | this.pending--; 36 | this[kRun](); 37 | }; 38 | this.concurrency = concurrency || Infinity; 39 | this.jobs = []; 40 | this.pending = 0; 41 | } 42 | 43 | /** 44 | * Adds a job to the queue. 45 | * 46 | * @public 47 | */ 48 | add(job) { 49 | this.jobs.push(job); 50 | this[kRun](); 51 | } 52 | 53 | /** 54 | * Removes a job from the queue and runs it if possible. 55 | * 56 | * @private 57 | */ 58 | [kRun]() { 59 | if (this.pending === this.concurrency) return; 60 | 61 | if (this.jobs.length) { 62 | const job = this.jobs.shift(); 63 | 64 | this.pending++; 65 | job(this[kDone]); 66 | } 67 | } 68 | } 69 | 70 | module.exports = Limiter; 71 | -------------------------------------------------------------------------------- /src/http/populate.js: -------------------------------------------------------------------------------- 1 | // populates missing values 2 | module.exports = function(dst, src) { 3 | 4 | Object.keys(src).forEach(function(prop) 5 | { 6 | dst[prop] = dst[prop] || src[prop]; 7 | }); 8 | 9 | return dst; 10 | }; 11 | -------------------------------------------------------------------------------- /src/http/response.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * response.js 19 | * 20 | * Response class provides content decoding 21 | */ 22 | 23 | // import http from 'http'; 24 | 25 | const Headers = require('./headers.js'); 26 | const Body = require('./body'); 27 | const extractContentType = Body.extractContentType; 28 | 29 | const INTERNALS = Symbol('Response internals'); 30 | 31 | 32 | 33 | /** 34 | * Expose `HttpResponse`. 35 | */ 36 | 37 | module.exports = HttpResponse; 38 | 39 | /** 40 | * Initialize a new `HttpResponse`. 41 | * 42 | * @api public 43 | */ 44 | 45 | function HttpResponse(body = null, opts = {}) { 46 | 47 | Body.call(this, body, opts); 48 | 49 | const status = opts.status || 200; 50 | const headers = new Headers(opts.headers) 51 | 52 | if (body !== null && !headers.has('Content-Type')) { 53 | let contentType; 54 | try { 55 | contentType = extractContentType(body); 56 | } catch (err) { 57 | // Sometimes we see this on 401 responses, so just ignore exception 58 | } 59 | if (contentType) { 60 | headers.append('Content-Type', contentType); 61 | } 62 | } 63 | 64 | this[INTERNALS] = { 65 | url: opts.url, 66 | status, 67 | // statusText: opts.statusText || STATUS_CODES[status], 68 | headers, 69 | counter: opts.counter 70 | }; 71 | 72 | var ctx = mixin(this); 73 | 74 | return ctx; 75 | } 76 | 77 | /** 78 | * Mixin the prototype properties. 79 | * 80 | * @param {Object} obj 81 | * @return {Object} 82 | * @api private 83 | */ 84 | 85 | function mixin(obj) { 86 | for (const key in HttpResponse.prototype) { 87 | if (Object.prototype.hasOwnProperty.call(HttpResponse.prototype, key)) 88 | obj[key] = HttpResponse.prototype[key]; 89 | } 90 | 91 | Object.defineProperty(obj, 'url', { 92 | get: function() { 93 | return this[INTERNALS].url || ''; 94 | } 95 | }); 96 | 97 | Object.defineProperty(obj, 'status', { 98 | get: function() { 99 | return this[INTERNALS].status; 100 | } 101 | }); 102 | 103 | Object.defineProperty(obj, 'ok', { 104 | get: function() { 105 | return this[INTERNALS].status >= 200 && this[INTERNALS].status < 300; 106 | } 107 | }); 108 | 109 | Object.defineProperty(obj, 'headers', { 110 | get: function() { 111 | return this[INTERNALS].headers; 112 | } 113 | }); 114 | 115 | return obj; 116 | } 117 | 118 | Body.mixIn(HttpResponse.prototype); 119 | -------------------------------------------------------------------------------- /src/http/validation.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | try { 20 | const isValidUTF8 = require('utf-8-validate'); 21 | 22 | exports.isValidUTF8 = 23 | typeof isValidUTF8 === 'object' 24 | ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 25 | : isValidUTF8; 26 | } catch (e) /* istanbul ignore next */ { 27 | exports.isValidUTF8 = () => true; 28 | } 29 | 30 | /** 31 | * Checks if a status code is allowed in a close frame. 32 | * 33 | * @param {Number} code The status code 34 | * @return {Boolean} `true` if the status code is valid, else `false` 35 | * @public 36 | */ 37 | exports.isValidStatusCode = (code) => { 38 | return ( 39 | (code >= 1000 && 40 | code <= 1014 && 41 | code !== 1004 && 42 | code !== 1005 && 43 | code !== 1006) || 44 | (code >= 3000 && code <= 4999) 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/http/ziti-agent.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var url = require('url'); 18 | const ZitiSocket = require('./ziti-socket'); 19 | 20 | /** 21 | * Module exports. 22 | */ 23 | 24 | module.exports = ZitiAgent; 25 | 26 | 27 | 28 | /** 29 | * Base HTTP "ZitiAgent" class. Emulates the node-core `http.Agent` class, but 30 | * implemented in a way that can be extended for additional functionality. 31 | * 32 | * 33 | * @api public 34 | */ 35 | 36 | function ZitiAgent (opts) { 37 | 38 | if (!(this instanceof ZitiAgent)) return new ZitiAgent(opts); 39 | if ('string' == typeof opts) opts = url.parse(opts); 40 | // Agent.call(this); 41 | this.proxy = opts; 42 | this.secure = this.proxy.protocol && this.proxy.protocol === 'https:'; 43 | // EventEmitter.call(this); 44 | 45 | } 46 | 47 | // inherits(ZitiAgent, EventEmitter); 48 | 49 | /** 50 | * Default port to connect to. 51 | */ 52 | 53 | ZitiAgent.prototype.defaultPort = 443; 54 | 55 | /** 56 | * Called when creating a new HTTP request with this ZitiAgent instance. 57 | * 58 | * @api public 59 | */ 60 | 61 | ZitiAgent.prototype.addRequest = function(req, host, port, localAddress) { 62 | 63 | let opts; 64 | if (typeof host == 'object') { 65 | // >= v0.11.x API 66 | opts = host; 67 | } else { 68 | // <= v0.10.x API 69 | opts = { 70 | host, 71 | port, 72 | localAddress, 73 | }; 74 | } 75 | 76 | // hint to use "Connection: close" 77 | req.shouldKeepAlive = false; 78 | 79 | // create the `ZitiSocket` instance 80 | const info = { 81 | serviceName: opts.serviceName, 82 | conn: opts.conn, 83 | host: opts.hostname || opts.host, 84 | port: Number(opts.port) || this.defaultPort, 85 | localAddress: opts.localAddress, 86 | isWebSocket: opts.isWebSocket, 87 | }; 88 | 89 | this.createConnection(info, (err, socket) => { 90 | if (err) { 91 | req.emit('error', err); 92 | } else { 93 | req.onSocket(socket); 94 | } 95 | }); 96 | } 97 | 98 | 99 | 100 | /** 101 | * Creates and returns a `ZitiSocket` instance to use for an HTTP request. 102 | * 103 | * @api public 104 | */ 105 | 106 | ZitiAgent.prototype.createConnection = function(opts, deferredFn) { 107 | this.deferredFn = deferredFn; 108 | const onSocketConnect = () => { 109 | this.deferredFn(null, this.socket); 110 | }; 111 | this.socket = new ZitiSocket( opts ); 112 | this.socket.connect(opts); 113 | this.socket.once('connect', onSocketConnect); 114 | }; 115 | -------------------------------------------------------------------------------- /src/http/ziti-socket.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const EventEmitter = require('events'); 18 | const isUndefined = require('lodash.isundefined'); 19 | 20 | 21 | class ZitiSocket extends EventEmitter { 22 | 23 | constructor(opts) { 24 | super(); 25 | 26 | /** 27 | * 28 | */ 29 | this.isWebSocket = false; 30 | if (typeof opts !== 'undefined') { 31 | if (typeof opts.isWebSocket !== 'undefined') { 32 | this.isWebSocket = opts.isWebSocket; 33 | } 34 | } 35 | 36 | /** 37 | * This stream is where we'll put any data returned from a Ziti connection (see ziti_dial.data.call_back) 38 | */ 39 | this.readableZitiStream = new ReadableStream({ 40 | start(controller) { 41 | self.readableZitiStreamController = controller; 42 | } 43 | }); 44 | 45 | 46 | /** 47 | * The underlying Ziti Connection 48 | * @private 49 | * @type {string} 50 | */ 51 | this.zitiConnection; 52 | 53 | 54 | /** 55 | * 56 | */ 57 | this._writable = false; 58 | } 59 | 60 | 61 | 62 | 63 | /** 64 | * Make a connection to the specified Ziti 'service'. We do this by invoking the ziti_dial() function in the Ziti NodeJS-SDK. 65 | * @param {*} service 66 | */ 67 | ziti_dial(service) { 68 | 69 | const self = this; 70 | return new Promise((resolve) => { 71 | if (self.zitiConnection) { 72 | resolve(self.zitiConnection); 73 | } 74 | else { 75 | window.ziti.ziti_dial( 76 | service, 77 | 78 | self.isWebSocket, 79 | 80 | /** 81 | * on_connect callback. 82 | */ 83 | (conn) => { 84 | // logger.info('on_connect callback: conn: %s', this.connAsHex(conn)) 85 | resolve(conn); 86 | }, 87 | 88 | /** 89 | * on_data callback 90 | */ 91 | (data) => { 92 | conn.getCtx().logger.trace('on_data callback: conn: %s, data: \n%s', this.connAsHex(this.zitiConnection), data.toString()); 93 | this.readableZitiStreamController.enqueue(data); 94 | }, 95 | ); 96 | } 97 | }); 98 | } 99 | 100 | /** 101 | * Write data onto the underlying Ziti connection by invoking the ziti_write() function in the Ziti NodeJS-SDK. The 102 | * NodeJS-SDK expects incoming data to be of type Buffer. 103 | */ 104 | ziti_write(conn, buffer) { 105 | return new Promise((resolve) => { 106 | window.ziti.ziti_write( 107 | conn, buffer, 108 | () => { 109 | resolve(); 110 | }, 111 | ); 112 | }); 113 | } 114 | 115 | /** 116 | * 117 | */ 118 | captureResponseData(conn, data) { 119 | 120 | conn.getCtx().logger.trace("captureResponseData() <- conn: [%d], dataLen: [%o]", conn.getId(), data.byteLength); 121 | // conn.getCtx().logger.trace("captureResponseData() <- conn: [%d], data: [%o]", conn.getId(), data); 122 | 123 | let zitiSocket = conn.getSocket(); 124 | 125 | conn.getCtx().logger.trace("captureResponseData() <- zitiSocket: [%o]", zitiSocket); 126 | 127 | if (data.byteLength > 0) { 128 | zitiSocket.emit('data', data); 129 | } else { 130 | zitiSocket.emit('close', data); 131 | } 132 | } 133 | 134 | /** 135 | * Connect to a Ziti service. 136 | */ 137 | async connect(opts) { 138 | 139 | if (typeof opts.conn == 'object') { 140 | this.zitiConnection = opts.conn; 141 | } 142 | else if (typeof opts.serviceName == 'string') { 143 | this.zitiConnection = ziti.newConnection(ziti._ctx); 144 | await ziti.dial(this.zitiConnection, opts.serviceName); 145 | this.zitiConnection.getCtx().logger.debug("ZitiSocket: connect: dial(%s) on conn[%d] now complete", opts.serviceName, this.zitiConnection.getId()); 146 | } else { 147 | throw new Error('no serviceName or conn was provided'); 148 | } 149 | 150 | this._writable = true; 151 | 152 | // Prepare to capture response data from the request we are about to launch 153 | this.zitiConnection.setDataCallback(this.captureResponseData); 154 | this.zitiConnection.setSocket(this); 155 | 156 | this.emit('connect', this.zitiConnection); 157 | } 158 | 159 | 160 | /** 161 | * 162 | */ 163 | _read() { /* NOP */ } 164 | read() { /* NOP */ } 165 | 166 | 167 | /** 168 | * 169 | */ 170 | destroy() { /* NOP */ } 171 | 172 | 173 | /** 174 | * Returna a Promise that will resolve _only_ after a Ziti connection has been established for this instance of ZitiSocket. 175 | */ 176 | getZitiConnection() { 177 | const self = this; 178 | return new Promise((resolve) => { 179 | (function waitForConnected() { 180 | if (self.zitiConnection && (!isUndefined(self.zitiConnection.getChannel()))) return resolve(self.zitiConnection); 181 | setTimeout(waitForConnected, 10); 182 | })(); 183 | }); 184 | } 185 | 186 | connAsHex(conn) { 187 | if (conn < 0) { 188 | conn = 0xFFFFFFFF + conn + 1; 189 | } 190 | return '0x' + conn.toString(16); 191 | } 192 | 193 | /** 194 | * Implements the writeable stream method `_write` by pushing the data onto the underlying Ziti connection. 195 | * It is possible that this function is called before the Ziti connect has completed, so this function will (currently) 196 | * await Ziti connection establishment (as opposed to buffering the data). 197 | */ 198 | async write(chunk, encoding, cb) { 199 | 200 | let buffer; 201 | 202 | if (typeof chunk === 'string' || chunk instanceof String) { 203 | buffer = Buffer.from(chunk, 'utf8'); 204 | } else if (Buffer.isBuffer(chunk)) { 205 | buffer = chunk; 206 | } else if (chunk instanceof Uint8Array) { 207 | buffer = Buffer.from(chunk, 'utf8'); 208 | } else { 209 | throw new Error('chunk type of [' + typeof chunk + '] is not a supported type'); 210 | } 211 | if (buffer.length > 0) { 212 | const conn = await this.getZitiConnection().catch((e) => conn.getCtx().logger.error('inside ziti-socket.js _write(), Error 1: ', e.message)); 213 | 214 | let ch = conn.getChannel(); 215 | 216 | // logger.info('_write: conn: %s, length: %s, data: \n%s', this.connAsHex(conn), buffer.byteLength, buffer.toString()); 217 | 218 | // await this.ziti_write(conn, buffer).catch((e) => logger.error('_write(), Error 2: ', e.message)); 219 | 220 | // let response = await 221 | ch.write(conn, buffer); 222 | 223 | } 224 | if (cb) { 225 | cb(); 226 | } 227 | } 228 | 229 | /** 230 | * 231 | */ 232 | cork() { 233 | this._writable = false; 234 | } 235 | uncork() { 236 | this._writable = true; 237 | } 238 | 239 | /** 240 | * 241 | */ 242 | pause() { 243 | this._writable = false; 244 | } 245 | resume() { 246 | this._writable = true; 247 | } 248 | 249 | /** 250 | * 251 | */ 252 | async destroy() { 253 | this._writable = false; 254 | await ziti.close(this.zitiConnection); 255 | } 256 | 257 | /** 258 | * 259 | */ 260 | async end(data, encoding, callback) { 261 | this._writable = false; 262 | await ziti.close(this.zitiConnection); 263 | } 264 | 265 | /** 266 | * Implements the writeable stream method `_final` used when .end() is called to write the final data to the stream. 267 | */ 268 | _final(cb) { 269 | cb(); 270 | } 271 | 272 | /** 273 | * 274 | */ 275 | setTimeout() { 276 | /* NOP */ 277 | } 278 | 279 | /** 280 | * 281 | */ 282 | setNoDelay() { 283 | /* NOP */ 284 | } 285 | 286 | /** 287 | * 288 | */ 289 | unshift(head) { 290 | /* NOP */ 291 | } 292 | 293 | } 294 | 295 | Object.defineProperty(ZitiSocket.prototype, 'writable', { 296 | get() { 297 | return ( 298 | this._writable 299 | ); 300 | } 301 | }); 302 | 303 | /** 304 | * Module exports. 305 | */ 306 | 307 | module.exports = ZitiSocket; 308 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./client'); 3 | 4 | -------------------------------------------------------------------------------- /src/logLevels.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const LogLevel = {} 18 | LogLevel[LogLevel.Fatal = 0] = 'fatal' 19 | LogLevel[LogLevel.Error = 0] = 'error' 20 | LogLevel[LogLevel.Warn = 1] = 'warn' 21 | LogLevel[LogLevel.Log = 2] = 'log' 22 | LogLevel[LogLevel.Info = 3] = 'info' 23 | LogLevel[LogLevel.Success = 3] = 'success' 24 | LogLevel[LogLevel.Ziti = 3] = 'ziti' 25 | LogLevel[LogLevel.Debug = 4] = 'debug' 26 | LogLevel[LogLevel.Trace = 5] = 'trace' 27 | LogLevel[LogLevel.Silent = -Infinity] = 'silent' 28 | LogLevel[LogLevel.Verbose = Infinity] = 'verbose' 29 | 30 | module.exports = LogLevel; -------------------------------------------------------------------------------- /src/pki/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const LogLevel = require('../logLevels'); 19 | 20 | /** 21 | * Default options. 22 | */ 23 | 24 | 25 | module.exports = { 26 | 27 | /** 28 | * See {@link Options.ctx} 29 | * 30 | */ 31 | ctx: null, 32 | 33 | /** 34 | * See {@link Options.logger} 35 | * 36 | */ 37 | logger: null, 38 | 39 | /** 40 | * See {@link Options.logLevel} 41 | * 42 | */ 43 | logLevel: LogLevel.Trace, 44 | 45 | }; -------------------------------------------------------------------------------- /src/pki/pki.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Module dependencies. 19 | */ 20 | 21 | const flatOptions = require('flat-options'); 22 | const forge = require('node-forge'); 23 | const isUndefined = require('lodash.isundefined'); 24 | const isNull = require('lodash.isnull'); 25 | const consola = require('consola'); 26 | let MicroModal; 27 | let modalMsg; 28 | if (typeof window !== 'undefined') { 29 | MicroModal = require('micromodal'); 30 | modalMsg = require('../ui/identity_modal/keypair_generation_msg'); 31 | } 32 | 33 | const ZitiReporter = require('../utils/ziti-reporter'); 34 | const ls = require('../utils/localstorage'); 35 | const defaultOptions = require('./options'); 36 | const zitiConstants = require('../constants'); 37 | let identityModalCSS; 38 | let keypairModalHTML; 39 | if (typeof window !== 'undefined') { 40 | identityModalCSS = require('../ui/identity_modal/css'); 41 | keypairModalHTML = require('../ui/identity_modal/keypair_generation_html'); 42 | } 43 | 44 | 45 | /** 46 | * Expose `ZitiPKI`. 47 | */ 48 | 49 | module.exports = ZitiPKI; 50 | 51 | /** 52 | * Initialize a new `ZitiPKI`. 53 | * 54 | * @api public 55 | */ 56 | 57 | function ZitiPKI(obj) { 58 | var ctx = mixin(obj); 59 | return ctx; 60 | } 61 | 62 | /** 63 | * Mixin the prototype properties. 64 | * 65 | * @param {Object} obj 66 | * @return {Object} 67 | * @api private 68 | */ 69 | function mixin(obj) { 70 | for (const key in ZitiPKI.prototype) { 71 | if (Object.prototype.hasOwnProperty.call(ZitiPKI.prototype, key)) 72 | obj[key] = ZitiPKI.prototype[key]; 73 | } 74 | 75 | return obj; 76 | } 77 | 78 | 79 | /** 80 | * Initialize the Ziti PKI. 81 | * 82 | * Tasks: 83 | * - validate options 84 | * - create logger if necessary 85 | * 86 | * @param {Object} [options] 87 | * @returns {nothing} 88 | */ 89 | ZitiPKI.prototype.init = async function(options) { 90 | 91 | let self = this; 92 | 93 | return new Promise( async (resolve, reject) => { 94 | 95 | let _options = flatOptions(options, defaultOptions); 96 | 97 | self.ctx = _options.ctx; 98 | self.logger = _options.logger; 99 | 100 | if (isNull(self.logger)) { 101 | self.logger = consola.create({ 102 | level: _options.logLevel, 103 | reporters: [ 104 | new ZitiReporter() 105 | ], 106 | defaults: { 107 | additionalColor: 'white' 108 | } 109 | }); 110 | self.logger.wrapConsole(); 111 | } 112 | 113 | resolve(); 114 | }); 115 | } 116 | 117 | 118 | ZitiPKI.prototype._haveKeypair = async function() { 119 | 120 | let self = this; 121 | 122 | return new Promise( async (resolve, reject) => { 123 | 124 | let privateKey = await ls.get(zitiConstants.get().ZITI_IDENTITY_PRIVATE_KEY); 125 | let publicKey = await ls.get(zitiConstants.get().ZITI_IDENTITY_PUBLIC_KEY); 126 | 127 | if ( 128 | isNull( privateKey ) || isUndefined( privateKey ) || 129 | isNull( publicKey ) || isUndefined( publicKey ) 130 | ) { 131 | resolve( false ); 132 | } else { 133 | resolve( true ); 134 | } 135 | }); 136 | 137 | } 138 | 139 | /** 140 | * Generate the keypair for this browser 141 | * 142 | * @params {nothing} 143 | * @returns {nothing} 144 | */ 145 | ZitiPKI.prototype.generateKeyPair = async function() { 146 | 147 | let self = this; 148 | 149 | let privateKeySize = 4096; 150 | let stepInterval = 500; // step-time interval, in ms, for generating keypair 151 | 152 | return new Promise( async (resolve, reject) => { 153 | 154 | let haveKeys = await self._haveKeypair(); 155 | if (haveKeys) { 156 | self.logger.trace('Pre-existing KeyPair found; skipping new keypair generation'); 157 | resolve( false ); 158 | return; 159 | } 160 | 161 | self.logger.info('Starting KeyPair Generation'); 162 | 163 | // if (typeof window !== 'undefined') { 164 | 165 | // identityModalCSS.inject(); 166 | // keypairModalHTML.inject(); 167 | // MicroModal.init({ 168 | // onShow: modal => console.info(`${modal.id} is shown`), // [1] 169 | // onClose: modal => console.info(`${modal.id} is hidden`), // [2] 170 | // openTrigger: 'ziti-data-micromodal-trigger', // [3] 171 | // closeTrigger: 'data-custom-close', // [4] 172 | // openClass: 'is-open', // [5] 173 | // disableScroll: true, // [6] 174 | // disableFocus: false, // [7] 175 | // awaitOpenAnimation: false, // [8] 176 | // awaitCloseAnimation: true, // [9] 177 | // debugMode: false // [10] 178 | // }); 179 | 180 | // MicroModal.show('ziti-keypair-modal'); 181 | 182 | // modalMsg.setMessage('Please do not close this browser window.'); 183 | 184 | // modalMsg.setProgress('Zero-Trust KeyPair creation in progress.'); 185 | // } 186 | 187 | 188 | var startTime, endTime; 189 | 190 | startTime = performance.now(); 191 | 192 | // Generate an RSA key pair, run for a few ms at a time on the main JS thread, so as not to completely block JS execution in browser. 193 | var state = forge.pki.rsa.createKeyPairGenerationState( privateKeySize ); 194 | 195 | var step = async function() { 196 | 197 | // If keypair generation still not complete 198 | if (!forge.pki.rsa.stepKeyPairGenerationState( state, stepInterval )) { 199 | 200 | endTime = performance.now(); 201 | var timeDiff = endTime - startTime; //in ms 202 | timeDiff /= 1000; // strip the ms 203 | var seconds = Math.round(timeDiff); 204 | 205 | if ((seconds % 2) == 0) { 206 | self.logger.debug('Zero-Trust KeyPair creation in progress: elapsed[' + seconds + ' sec]'); 207 | if (typeof window !== 'undefined') { 208 | modalMsg.setProgress('Zero-Trust KeyPair creation in progress: elapsed[' + seconds + ' sec]'); 209 | } 210 | } 211 | 212 | setTimeout(step, 50); 213 | 214 | } else { // Now that we have a keypair 215 | 216 | self._privateKey = state.keys.privateKey 217 | self._publicKey = state.keys.publicKey 218 | 219 | let privatePEM = forge.pki.privateKeyToPem(self._privateKey); 220 | privatePEM = privatePEM.replace(/\\n/g, '\n'); 221 | privatePEM = privatePEM.replace(/[\r]+/g, ''); 222 | privatePEM = privatePEM.replace(/\n/g, '\x0a'); 223 | await ls.setWithExpiry(zitiConstants.get().ZITI_IDENTITY_PRIVATE_KEY, privatePEM, new Date(8640000000000000)); 224 | 225 | let publicPEM = forge.pki.publicKeyToPem(self._publicKey); 226 | publicPEM = publicPEM.replace(/\\n/g, '\n'); 227 | publicPEM = publicPEM.replace(/[\r]+/g, ''); 228 | publicPEM = publicPEM.replace(/\n/g, '\x0a'); 229 | await ls.setWithExpiry(zitiConstants.get().ZITI_IDENTITY_PUBLIC_KEY, publicPEM, new Date(8640000000000000)); 230 | 231 | endTime = performance.now(); 232 | var timeDiff = endTime - startTime; //in ms 233 | timeDiff /= 1000; // strip the ms 234 | var seconds = Math.round(timeDiff); 235 | 236 | self.logger.info('KeyPair Generation COMPLETE... elapsed[' + seconds + ' sec]'); 237 | 238 | // if (typeof window !== 'undefined') { 239 | // modalMsg.setProgress('KeyPair Generation COMPLETE... elapsed[' + seconds + ' sec]'); 240 | 241 | // modalMsg.setMessage('You may now REFRESH this browser window to load the application.'); 242 | 243 | // MicroModal.close('ziti-keypair-modal'); 244 | // } 245 | 246 | resolve( true ); 247 | } 248 | }; 249 | 250 | setTimeout(step); // initiate async keypair generation 251 | }); 252 | 253 | } 254 | 255 | 256 | /** 257 | * Wait for keypair generation to complete before returning 258 | * 259 | * @params {nothing} 260 | * @returns {nothing} 261 | */ 262 | ZitiPKI.prototype.awaitKeyPairGenerationComplete = async function( bypassRenderingOfUI ) { 263 | 264 | let self = this; 265 | 266 | return new Promise( async (resolve, reject) => { 267 | 268 | let haveKeys = await self._haveKeypair(); 269 | if (haveKeys) { 270 | self.logger.info('Pre-existing KeyPair found; skipping wait for keypair generation completion'); 271 | resolve( false ); 272 | return; 273 | } 274 | 275 | if (typeof window !== 'undefined') { 276 | 277 | if (bypassRenderingOfUI) { 278 | self.logger.info('bypassRenderingOfUI'); 279 | } else { 280 | 281 | identityModalCSS.inject(); 282 | keypairModalHTML.inject(); 283 | MicroModal.init({ 284 | onShow: modal => console.info(`${modal.id} is shown`), // [1] 285 | onClose: modal => console.info(`${modal.id} is hidden`), // [2] 286 | openTrigger: 'ziti-data-micromodal-trigger', // [3] 287 | closeTrigger: 'data-custom-close', // [4] 288 | openClass: 'is-open', // [5] 289 | disableScroll: true, // [6] 290 | disableFocus: false, // [7] 291 | awaitOpenAnimation: false, // [8] 292 | awaitCloseAnimation: true, // [9] 293 | debugMode: false // [10] 294 | }); 295 | 296 | MicroModal.show('ziti-keypair-modal'); 297 | 298 | modalMsg.setMessage('Please do not close this browser window.'); 299 | 300 | modalMsg.setProgress('Zero-Trust KeyPair creation in progress.'); 301 | } 302 | } 303 | 304 | 305 | (async function waitForKeyPairGenerationComplete() { 306 | let haveKeys = await self._haveKeypair(); 307 | if (haveKeys) { 308 | MicroModal.close('ziti-keypair-modal'); 309 | return resolve(true); 310 | } 311 | setTimeout(waitForKeyPairGenerationComplete, 200); 312 | })(); 313 | 314 | }); 315 | } 316 | -------------------------------------------------------------------------------- /src/ui/identity_modal/css.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | 19 | /** 20 | * Inject CSS needed for the Identity Modal. 21 | * 22 | */ 23 | exports.inject = () => { 24 | 25 | styleString = ` 26 | 27 | // body { 28 | // background: #eee !important; 29 | // } 30 | 31 | * { 32 | -webkit-box-sizing: border-box; 33 | -moz-box-sizing: border-box; 34 | box-sizing: border-box; 35 | } 36 | 37 | .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { 38 | font-family: inherit; 39 | font-weight: 500; 40 | line-height: 1.1; 41 | color: inherit; 42 | } 43 | 44 | .wrapper { 45 | margin-top: 80px; 46 | margin-bottom: 80px; 47 | position: relative; 48 | z-index: 999; 49 | } 50 | 51 | .form-signin { 52 | border-radius: 10px; 53 | max-width: 380px; 54 | padding: 15px 35px 45px; 55 | margin: 0 auto; 56 | background-color: #fff; 57 | border: 1px solid rgba(0, 0, 0, 0.1); 58 | box-sizing: border-box; 59 | } 60 | .form-signin .form-signin-heading, 61 | .form-signin .checkbox { 62 | margin-bottom: 30px; 63 | font-size: 18px; 64 | color: black; 65 | font-family: sans-serif; 66 | } 67 | .form-signin .checkbox { 68 | font-weight: normal; 69 | } 70 | .form-signin .form-control { 71 | position: relative; 72 | font-size: 16px; 73 | height: auto; 74 | padding: 10px; 75 | -webkit-box-sizing: border-box; 76 | -moz-box-sizing: border-box; 77 | box-sizing: border-box; 78 | } 79 | .form-signin .form-control:focus { 80 | z-index: 2; 81 | } 82 | .form-signin input[type="text"] { 83 | margin-bottom: -1px; 84 | border-bottom-left-radius: 0; 85 | border-bottom-right-radius: 0; 86 | } 87 | .form-signin input[type="password"] { 88 | margin-bottom: 20px; 89 | border-top-left-radius: 0; 90 | border-top-right-radius: 0; 91 | } 92 | 93 | ::placeholder { 94 | color: #9e9e9e; 95 | } 96 | 97 | .form-signin-button { 98 | background-image: linear-gradient(to bottom right, #082481 , #e00043); 99 | } 100 | 101 | .modal { 102 | font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif; 103 | background-image: linear-gradient(to bottom right, #082481 , #e00043); 104 | position: fixed; 105 | top: 0; 106 | right: 0; 107 | bottom: 0; 108 | left: 0; 109 | z-index: 1050; 110 | display: none; 111 | overflow: hidden; 112 | -webkit-overflow-scrolling: touch; 113 | outline: 0; 114 | } 115 | 116 | 117 | .modal__overlay { 118 | position: fixed; 119 | top: 0; 120 | left: 0; 121 | right: 0; 122 | bottom: 0; 123 | background: rgba(0,0,0,0.6); 124 | display: flex; 125 | justify-content: center; 126 | align-items: center; 127 | } 128 | 129 | .modal__container { 130 | background-color: #fff; 131 | padding: 30px; 132 | /* max-width: 500px; */ 133 | max-height: 100vh; 134 | border-radius: 4px; 135 | overflow-y: auto; 136 | box-sizing: border-box; 137 | } 138 | 139 | .modal__header { 140 | display: flex; 141 | justify-content: space-between; 142 | align-items: center; 143 | border-bottom: 1px solid #00449e; 144 | padding-bottom: 8px; 145 | position: relative; 146 | background: white 147 | } 148 | 149 | .modal__title { 150 | margin-top: 0; 151 | margin-bottom: 0; 152 | font-weight: 600; 153 | font-size: 1.0rem; 154 | line-height: 1.0; 155 | color: #000000; 156 | box-sizing: border-box; 157 | font-family: sans-serif; 158 | } 159 | 160 | .modal__title span { 161 | display: block; 162 | position: absolute; 163 | height: 16px; 164 | top: 50%; 165 | margin-top: -14px; 166 | margin-left: 32px; 167 | font-size: 18px; 168 | } 169 | 170 | 171 | .modal__close { 172 | background: transparent; 173 | border: 0; 174 | } 175 | 176 | .modal__header .modal__close:before { content: "\\2715"; } 177 | 178 | .modal__content { 179 | margin-top: 2rem; 180 | margin-bottom: 2rem; 181 | line-height: 1.5; 182 | color: rgba(0,0,0,.8); 183 | } 184 | 185 | .modal__content p { 186 | text-align: center; 187 | } 188 | 189 | label.modal__upload { 190 | width: 100%; 191 | } 192 | 193 | .modal__btn { 194 | font-size: .875rem; 195 | padding-left: 1rem; 196 | padding-right: 1rem; 197 | padding-top: .5rem; 198 | padding-bottom: .5rem; 199 | background-color: #e6e6e6; 200 | color: rgba(0,0,0,.8); 201 | border-radius: .25rem; 202 | border-style: none; 203 | border-width: 0; 204 | cursor: pointer; 205 | -webkit-appearance: button; 206 | text-transform: none; 207 | overflow: visible; 208 | line-height: 1.15; 209 | margin: 0; 210 | will-change: transform; 211 | -moz-osx-font-smoothing: grayscale; 212 | -webkit-backface-visibility: hidden; 213 | backface-visibility: hidden; 214 | -webkit-transform: translateZ(0); 215 | transform: translateZ(0); 216 | transition: -webkit-transform .25s ease-out; 217 | transition: transform .25s ease-out; 218 | transition: transform .25s ease-out,-webkit-transform .25s ease-out; 219 | } 220 | 221 | .modal__btn:focus, .modal__btn:hover { 222 | -webkit-transform: scale(1.05); 223 | transform: scale(1.05); 224 | } 225 | 226 | .modal__btn-primary { 227 | background-color: #00449e; 228 | color: #fff; 229 | } 230 | 231 | 232 | 233 | /**************************\ 234 | Demo Animation Style 235 | \**************************/ 236 | @keyframes mmfadeIn { 237 | from { opacity: 0; } 238 | to { opacity: 1; } 239 | } 240 | 241 | @keyframes mmfadeOut { 242 | from { opacity: 1; } 243 | to { opacity: 0; } 244 | } 245 | 246 | @keyframes mmslideIn { 247 | from { transform: translateY(15%); } 248 | to { transform: translateY(0); } 249 | } 250 | 251 | @keyframes mmslideOut { 252 | from { transform: translateY(0); } 253 | to { transform: translateY(-10%); } 254 | } 255 | 256 | .micromodal-slide { 257 | display: none; 258 | } 259 | 260 | .micromodal-slide.is-open { 261 | display: block; 262 | } 263 | 264 | .micromodal-slide[aria-hidden="false"] .modal__overlay { 265 | animation: mmfadeIn .3s cubic-bezier(0.0, 0.0, 0.2, 1); 266 | } 267 | 268 | .micromodal-slide[aria-hidden="false"] .modal__container { 269 | animation: mmslideIn .3s cubic-bezier(0, 0, .2, 1); 270 | } 271 | 272 | .micromodal-slide[aria-hidden="true"] .modal__overlay { 273 | animation: mmfadeOut .3s cubic-bezier(0.0, 0.0, 0.2, 1); 274 | } 275 | 276 | .micromodal-slide[aria-hidden="true"] .modal__container { 277 | animation: mmslideOut .3s cubic-bezier(0, 0, .2, 1); 278 | } 279 | 280 | .micromodal-slide .modal__container, 281 | .micromodal-slide .modal__overlay { 282 | will-change: transform; 283 | } 284 | 285 | .ziti-footer { 286 | font-family: 'Roboto', 'Noto', sans-serif; 287 | font-weight: 400; 288 | -webkit-font-smoothing: antialiased; 289 | letter-spacing: 0.011em; 290 | font-size: 12px; 291 | line-height: 16px; 292 | color: white; 293 | text-align: center; 294 | } 295 | 296 | .btn-block { 297 | display: block; 298 | width: 100%; 299 | } 300 | .btn-group-lg>.btn, .btn-lg { 301 | padding: 10px 16px; 302 | font-size: 18px; 303 | line-height: 1.3333333; 304 | border-radius: 6px; 305 | } 306 | .btn-primary { 307 | color: #fff; 308 | background-color: #337ab7; 309 | border-color: #2e6da4; 310 | } 311 | .btn { 312 | display: inline-block; 313 | padding: 6px 12px; 314 | margin-bottom: 0; 315 | font-size: 14px; 316 | font-weight: 400; 317 | line-height: 1.42857143; 318 | text-align: center; 319 | white-space: nowrap; 320 | vertical-align: middle; 321 | -ms-touch-action: manipulation; 322 | touch-action: manipulation; 323 | cursor: pointer; 324 | -webkit-user-select: none; 325 | -moz-user-select: none; 326 | -ms-user-select: none; 327 | user-select: none; 328 | // background-image: none; 329 | border: 1px solid transparent; 330 | border-radius: 4px; 331 | font-family: sans-serif; 332 | } 333 | 334 | button, input, select, textarea { 335 | font-family: inherit; 336 | font-size: inherit; 337 | line-height: inherit; 338 | } 339 | button, html input[type=button], input[type=reset], input[type=submit] { 340 | -webkit-appearance: button; 341 | cursor: pointer; 342 | } 343 | button, select { 344 | text-transform: none; 345 | } 346 | button { 347 | overflow: visible; 348 | } 349 | button, input, optgroup, select, textarea { 350 | margin: 0; 351 | font: inherit; 352 | color: inherit; 353 | } 354 | button, input, select, textarea { 355 | font-family: inherit; 356 | font-size: inherit; 357 | line-height: inherit; 358 | } 359 | button, html input[type=button], input[type=reset], input[type=submit] { 360 | -webkit-appearance: button; 361 | cursor: pointer; 362 | } 363 | button, select { 364 | text-transform: none; 365 | } 366 | button { 367 | overflow: visible; 368 | } 369 | button, input, optgroup, select, textarea { 370 | margin: 0; 371 | font: inherit; 372 | color: inherit; 373 | } 374 | * { 375 | -webkit-box-sizing: border-box; 376 | -moz-box-sizing: border-box; 377 | box-sizing: border-box; 378 | } 379 | * { 380 | -webkit-box-sizing: border-box; 381 | -moz-box-sizing: border-box; 382 | box-sizing: border-box; 383 | } 384 | 385 | .form-control { 386 | display: block; 387 | width: 100%; 388 | height: 34px; 389 | padding: 6px 12px; 390 | font-size: 14px; 391 | line-height: 1.42857143; 392 | color: #555; 393 | background-color: #fff; 394 | background-image: none; 395 | border: 1px solid #ccc; 396 | border-radius: 4px; 397 | -webkit-box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%); 398 | box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%); 399 | -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; 400 | -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; 401 | transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; 402 | font-family: sans-serif; 403 | } 404 | `; 405 | 406 | const style = document.createElement('style'); 407 | style.textContent = styleString; 408 | document.head.append(style); 409 | 410 | // document.head.insertAdjacentHTML('afterbegin', ` 411 | // 412 | // `); 413 | 414 | } 415 | -------------------------------------------------------------------------------- /src/ui/identity_modal/dragdrop.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const error = require('./error'); 19 | const dragDrop = require('drag-drop'); 20 | const fileParser = require('./file-parser'); 21 | 22 | 23 | /** 24 | * Inject JS drag-drop-handler for the Identity Modal. 25 | * 26 | */ 27 | exports.injectDragDropHandler = () => { 28 | 29 | dragDrop('#animation', (files, pos, fileList, directories) => { 30 | 31 | let file = files[0]; 32 | 33 | if (file) { 34 | 35 | fileParser.parse(file); 36 | 37 | } else { 38 | error.setMessage('No file was selected'); 39 | } 40 | 41 | }); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/identity_modal/file-parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const jwt_decode = require('jwt-decode'); 18 | const ls = require('../../utils/localstorage'); 19 | const zitiConstants = require('../../constants'); 20 | 21 | let jwt = ''; 22 | 23 | let chunkSize = 1000; 24 | 25 | 26 | /** 27 | * Parse the selected identity.json file and save data we need to local storage 28 | * 29 | * @param {File} file 30 | */ 31 | exports.parse = (file) => { 32 | var fileSize = file.size; 33 | var offset = 0; 34 | var self = this; // we need a reference to the current object 35 | var chunkReaderBlock = null; 36 | 37 | var readEventHandler = function(evt) { 38 | if (evt.target.error === null) { 39 | offset += evt.target.result.length; 40 | receiveFileChunk(evt.target.result); // callback for handling read chunk 41 | } else { 42 | console.error("Read error: " + evt.target.error); 43 | return; 44 | } 45 | if (offset >= fileSize) { 46 | receiveEOF(); // callback for handling EOF 47 | return; 48 | } 49 | 50 | // off to the next chunk 51 | chunkReaderBlock(offset, chunkSize, file); 52 | } 53 | 54 | chunkReaderBlock = function(_offset, length, _file) { 55 | var r = new FileReader(); 56 | var blob = _file.slice(_offset, length + _offset); 57 | r.onload = readEventHandler; 58 | r.readAsText(blob); 59 | } 60 | 61 | // now let's start the read with the first block 62 | chunkReaderBlock(offset, chunkSize, file); 63 | } 64 | 65 | /** 66 | * Dispatch the chunk 67 | * 68 | * @param {string} chunk 69 | */ 70 | function receiveFileChunk(chunk) { 71 | jwt += chunk; 72 | } 73 | 74 | 75 | /** 76 | * Process results of parsing 77 | * 78 | * @param {string} chunk 79 | */ 80 | function receiveEOF() { 81 | let decoded_jwt = jwt_decode(jwt); 82 | ls.setWithExpiry(zitiConstants.get().ZITI_JWT, jwt, decoded_jwt.exp * 1000); 83 | } 84 | -------------------------------------------------------------------------------- /src/ui/identity_modal/keypair_generation_html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const MicroModal = require('micromodal'); 19 | const isNull = require('lodash.isnull'); 20 | const isUndefined = require('lodash.isundefined'); 21 | 22 | /** 23 | * Inject HTML needed for the KeyPair Modal. 24 | * 25 | */ 26 | exports.inject = () => { 27 | 28 | let self = this; 29 | 30 | htmlString = ` 31 | 57 | `; 58 | 59 | if (isNull(document.body) || isUndefined(document.body)) { 60 | var body = document.createElement("body"); 61 | document.documentElement.appendChild(body); 62 | } 63 | 64 | document.body.insertAdjacentHTML('afterbegin', htmlString); 65 | } 66 | -------------------------------------------------------------------------------- /src/ui/identity_modal/keypair_generation_msg.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const isNull = require('lodash.isnull'); 18 | 19 | 20 | /** 21 | * Inject err msg into the Identity Modal. 22 | * 23 | */ 24 | exports.setMessage = (errorMessage) => { 25 | 26 | var el = document.getElementById("ziti-keypair-error"); 27 | if (!isNull(el)) { 28 | if (typeof errorMessage != "undefined") { 29 | el.textContent = errorMessage; 30 | } else { 31 | el.textContent = ""; 32 | } 33 | } 34 | } 35 | 36 | exports.setProgress = (progressMessage) => { 37 | 38 | var el = document.getElementById("ziti-keypair-progress"); 39 | if (!isNull(el)) { 40 | if (typeof progressMessage != "undefined") { 41 | el.textContent = progressMessage; 42 | } else { 43 | el.textContent = ""; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ui/identity_modal/reloading_page_html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const MicroModal = require('micromodal'); 19 | const isNull = require('lodash.isnull'); 20 | const isUndefined = require('lodash.isundefined'); 21 | 22 | /** 23 | * Inject HTML needed for the 'Reloading Page' Modal. 24 | * 25 | */ 26 | exports.inject = () => { 27 | 28 | let self = this; 29 | 30 | htmlString = ` 31 | 51 | `; 52 | 53 | if (isNull(document.body) || isUndefined(document.body)) { 54 | var body = document.createElement("body"); 55 | document.documentElement.appendChild(body); 56 | } 57 | 58 | document.body.insertAdjacentHTML('afterbegin', htmlString); 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/identity_modal/select.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const error = require('./error'); 19 | const fileParser = require('./file-parser'); 20 | 21 | /** 22 | * Inject JS select change-handler for the Identity Modal. 23 | * 24 | */ 25 | exports.injectChangeHandler = () => { 26 | 27 | let imageUpload = document.getElementById("upload"); 28 | 29 | imageUpload.onchange = function() { 30 | 31 | let file = this.files[0]; 32 | 33 | if (file) { 34 | 35 | fileParser.parse(file); 36 | 37 | } else { 38 | error.setMessage('No file was selected'); 39 | } 40 | 41 | }; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/identity_modal/updb_prompt_html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const MicroModal = require('micromodal'); 19 | const isNull = require('lodash.isnull'); 20 | const isUndefined = require('lodash.isundefined'); 21 | 22 | // const dragDrop = require('drag-drop'); 23 | 24 | 25 | /** 26 | * Inject HTML needed for the Identity Modal. 27 | * 28 | */ 29 | exports.inject = () => { 30 | 31 | let self = this; 32 | 33 | htmlString = ` 34 | 35 | 70 | 71 | `; 72 | 73 | if (isNull(document.body) || isUndefined(document.body)) { 74 | var body = document.createElement("body"); 75 | document.documentElement.appendChild(body); 76 | } 77 | 78 | document.body.insertAdjacentHTML('afterbegin', htmlString); 79 | 80 | // let div = document.createElement('div'); 81 | // div.innerHTML = htmlString; 82 | // document.body.append(div); 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/ui/identity_modal/updb_prompt_keypairDirectory_html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const MicroModal = require('micromodal'); 19 | const isNull = require('lodash.isnull'); 20 | const isUndefined = require('lodash.isundefined'); 21 | 22 | // const dragDrop = require('drag-drop'); 23 | 24 | 25 | /** 26 | * Inject HTML needed for the Identity Modal. 27 | * 28 | */ 29 | exports.inject = ( text ) => { 30 | 31 | let self = this; 32 | 33 | htmlString = ` 34 | 35 | 68 | 69 | `; 70 | 71 | if (isNull(document.body) || isUndefined(document.body)) { 72 | var body = document.createElement("body"); 73 | document.documentElement.appendChild(body); 74 | } 75 | 76 | document.body.insertAdjacentHTML('afterbegin', htmlString); 77 | 78 | // let div = document.createElement('div'); 79 | // div.innerHTML = htmlString; 80 | // document.body.append(div); 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/updb/error.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const isNull = require('lodash.isnull'); 18 | 19 | 20 | /** 21 | * Inject err msg into the Identity Modal. 22 | * 23 | */ 24 | exports.setMessage = (errorMessage) => { 25 | 26 | var el = document.getElementById("ziti-identity-error") 27 | if (!isNull(el)) { 28 | if (typeof errorMessage != "undefined") { 29 | el.textContent = errorMessage 30 | el.style.color = "red" 31 | el = document.getElementById("ziti-identity-progress") 32 | if (!isNull(el)) { 33 | el.textContent = "" 34 | } 35 | } else { 36 | el.textContent = "" 37 | } 38 | } 39 | } 40 | 41 | exports.setProgress = (progressMessage) => { 42 | 43 | var el = document.getElementById("ziti-identity-progress") 44 | if (!isNull(el)) { 45 | if (typeof progressMessage != "undefined") { 46 | el.textContent = progressMessage 47 | el.style.color = "white" 48 | } else { 49 | el.textContent = "" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/updb/login.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const isUndefined = require('lodash.isundefined'); 18 | const error = require('./error'); 19 | 20 | /** 21 | * Inject JS select change-handler for the Identity Modal. 22 | * 23 | */ 24 | exports.injectButtonHandler = (cb) => { 25 | 26 | let loginButton = document.getElementById("ziti-login-button"); 27 | 28 | loginButton.onclick = function(e) { 29 | 30 | e.preventDefault(); 31 | 32 | let results = formValidation(); 33 | if (!isUndefined( results )) { 34 | cb(results); 35 | } 36 | }; 37 | } 38 | 39 | function formValidation() 40 | { 41 | let username = document.zitilogin.username; 42 | var username_len = username.value.length; 43 | if (username_len == 0) { 44 | error.setMessage('ERROR: Please specify a Username'); 45 | return undefined; 46 | } 47 | if (username_len < 4) { 48 | error.setMessage('ERROR: Username must be at least 4 characters long'); 49 | return undefined; 50 | } 51 | 52 | let password = document.zitilogin.password; 53 | var password_len = password.value.length; 54 | if (password_len == 0) { 55 | error.setMessage('ERROR: Please specify a Password'); 56 | return undefined; 57 | } 58 | if (password_len < 4) { 59 | error.setMessage('ERROR: Password must be at least 4 characters long'); 60 | return undefined; 61 | } 62 | 63 | error.setMessage(''); 64 | 65 | return { username: username.value, password: password.value }; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/updb/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const LogLevel = require('../logLevels'); 19 | 20 | /** 21 | * Default options. 22 | */ 23 | 24 | 25 | module.exports = { 26 | 27 | /** 28 | * See {@link Options.ctx} 29 | * 30 | */ 31 | ctx: null, 32 | 33 | /** 34 | * See {@link Options.logger} 35 | * 36 | */ 37 | logger: null, 38 | 39 | /** 40 | * See {@link Options.logLevel} 41 | * 42 | */ 43 | logLevel: LogLevel.Trace, 44 | 45 | }; -------------------------------------------------------------------------------- /src/utils/localstorage.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const isNull = require('lodash.isnull'); 18 | const localforage = require('localforage'); 19 | 20 | localforage.config({ 21 | driver : localforage.INDEXEDDB, 22 | name : 'ziti_sdk_js', 23 | version : 1.0, 24 | storeName : 'ziti_sdk_js_db', // Should be alphanumeric, with underscores. 25 | description : 'Ziti JS SDK database' 26 | }); 27 | 28 | 29 | /** 30 | * Save specified value under specified key 31 | * as well as the time when it's supposed to lazily expire 32 | * 33 | * @param {Object} key 34 | * @param {Object} value 35 | * @param {Object} ttl 36 | */ 37 | exports.setWithExpiry = async (key, value, ttl) => { 38 | return new Promise( async (resolve, reject) => { 39 | if (isNull( ttl )) { 40 | ttl = new Date(8640000000000000); 41 | } 42 | if (Object.prototype.toString.call(ttl) === '[object Date]') { 43 | ttl = ttl.getTime(); 44 | } 45 | const item = { 46 | value: value, 47 | expiry: ttl, 48 | expiryDate: new Date( ttl ), 49 | } 50 | await localforage.setItem(key, item); 51 | resolve(); 52 | }); 53 | } 54 | 55 | /** 56 | * Return value for specified key 57 | * or null if not found, or expired. 58 | * 59 | * @param {Object} key 60 | * @return {Object} value 61 | */ 62 | exports.getWithExpiry = async (key) => { 63 | return new Promise( async (resolve, reject) => { 64 | 65 | const item = await localforage.getItem(key) 66 | // if the item doesn't exist, return null 67 | if (isNull(item)) { 68 | resolve( null ); 69 | } else { 70 | const now = new Date() 71 | // compare the expiry time of the item with the current time 72 | if ( (!isNull(item.expiry)) && (now.getTime() > item.expiry) ) { 73 | // If the item is expired, delete the item from storage and return null 74 | await localforage.removeItem(key) 75 | resolve( null ); 76 | } else { 77 | resolve( item.value ); 78 | } 79 | } 80 | }); 81 | } 82 | 83 | /** 84 | * Return expiry value for specified key 85 | * or null if not found, or expired. 86 | * 87 | * @param {Object} key 88 | * @return {Object} expiry value 89 | */ 90 | exports.getExpiry = async (key) => { 91 | return new Promise( async (resolve, reject) => { 92 | 93 | const item = await localforage.getItem(key) 94 | // if the item doesn't exist, return null 95 | if (isNull(item)) { 96 | resolve( null ); 97 | } else { 98 | const now = new Date() 99 | // compare the expiry time of the item with the current time 100 | if ( (!isNull(item.expiry)) && (now.getTime() > item.expiry) ) { 101 | // If the item is expired, delete the item from storage and return null 102 | await localforage.removeItem(key) 103 | resolve( null ); 104 | } else { 105 | resolve( item.expiry ); 106 | } 107 | } 108 | }); 109 | } 110 | 111 | /** 112 | * Return value for specified key 113 | * or null if not found. 114 | * 115 | * @param {Object} key 116 | * @return {Object} value 117 | */ 118 | exports.get = async (key) => { 119 | return new Promise( async (resolve, reject) => { 120 | const item = await localforage.getItem(key) 121 | // if the item doesn't exist, return null 122 | if (isNull(item)) { 123 | resolve( null ); 124 | } else { 125 | resolve(item.value); 126 | } 127 | }); 128 | } 129 | 130 | 131 | /** 132 | * Remove value for specified key 133 | * 134 | * @param {*} key 135 | */ 136 | exports.removeItem = async (key) => { 137 | await localforage.removeItem( key ); 138 | } 139 | -------------------------------------------------------------------------------- /src/utils/pki.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const asn1js = require('asn1js'); 19 | const pkijs = require("pkijs"); 20 | const Certificate = pkijs.Certificate; 21 | 22 | 23 | 24 | /** 25 | * Convert base64 string to buffer 26 | * 27 | * @param {string} b64str 28 | */ 29 | exports.base64StringToArrayBuffer = (b64str) => { 30 | let byteStr = atob(b64str); 31 | let bytes = new Uint8Array(byteStr.length); 32 | for (let i = 0; i < byteStr.length; i++) { 33 | bytes[i] = byteStr.charCodeAt(i); 34 | } 35 | return bytes.buffer; 36 | } 37 | 38 | 39 | /** 40 | * Convert PEM string to binary 41 | * 42 | * @param {string} pem 43 | */ 44 | exports.convertPemToBinary = (pem) => { 45 | var lines = pem.split('\n'); 46 | var encoded = ''; 47 | for(var i = 0;i < lines.length;i++){ 48 | if (lines[i].trim().length > 0 && 49 | lines[i].indexOf('-BEGIN RSA PRIVATE KEY-') < 0 && 50 | lines[i].indexOf('-BEGIN RSA PUBLIC KEY-') < 0 && 51 | lines[i].indexOf('-BEGIN PUBLIC KEY-') < 0 && 52 | lines[i].indexOf('-BEGIN CERTIFICATE-') < 0 && 53 | lines[i].indexOf('-BEGIN PRIVATE KEY-') < 0 && 54 | lines[i].indexOf('-END PRIVATE KEY-') < 0 && 55 | lines[i].indexOf('-END CERTIFICATE-') < 0 && 56 | lines[i].indexOf('-END PUBLIC KEY-') < 0 && 57 | lines[i].indexOf('-END RSA PRIVATE KEY-') < 0 && 58 | lines[i].indexOf('-END RSA PUBLIC KEY-') < 0) { 59 | 60 | encoded += lines[i].trim(); 61 | 62 | } 63 | } 64 | return exports.base64StringToArrayBuffer(encoded); 65 | } 66 | 67 | 68 | /** 69 | * Convert buffer to Certificate 70 | * 71 | * @param {Buffer} certificateBuffer 72 | */ 73 | exports.convertBinaryToCertificate = (certificateBuffer) => { 74 | let asn1 = asn1js.fromBER(certificateBuffer); 75 | if(asn1.offset === (-1)) { 76 | console.log("Can not parse binary data"); 77 | } 78 | const certificate = new Certificate({ schema: asn1.result }); 79 | return certificate; 80 | } 81 | 82 | 83 | /** 84 | * Convert PEM to Certificate 85 | * 86 | * @param {string} pem 87 | */ 88 | exports.convertPemToCertificate = (pem) => { 89 | return exports.convertBinaryToCertificate( exports.convertPemToBinary(pem) ); 90 | } 91 | 92 | /** 93 | * Convert buffer to Certificate 94 | * 95 | * @param {string} pem 96 | */ 97 | exports.printCertificate = (certificate) => { 98 | console.log(certificate); 99 | console.log('Certificate Serial Number'); 100 | console.log('Certificate Issuance'); 101 | console.log(certificate.notBefore.value.toString()); 102 | console.log('Certificate Expiry'); 103 | console.log(certificate.notAfter.value.toString()); 104 | console.log(certificate.issuer); 105 | } 106 | 107 | 108 | /** 109 | * Return time (in millis) for when Certificate expires 110 | * 111 | * @param {Buffer} certificateBuffer 112 | */ 113 | exports.getExpiryTimeFromCertificate = (certificate) => { 114 | return certificate.notAfter.toSchema().toDate().getTime(); 115 | } 116 | 117 | 118 | /** 119 | * Return time (human-readable) for when Certificate expires 120 | * 121 | * @param {Buffer} certificateBuffer 122 | */ 123 | exports.getExpiryStringFromCertificate = (certificate) => { 124 | return certificate.notAfter.toSchema().toDate().toString(); 125 | } 126 | 127 | 128 | /** 129 | * Return time (in millis) for when Certificate becomes usable 130 | * 131 | * @param {Buffer} certificateBuffer 132 | */ 133 | exports.getBecomesUsableTimeFromCertificate = (certificate) => { 134 | return certificate.notBefore.toSchema().toDate().getTime(); 135 | } 136 | 137 | 138 | /** 139 | * Return time (human-readable) for when Certificate becomes usable 140 | * 141 | * @param {Buffer} certificateBuffer 142 | */ 143 | exports.getBecomesUsableStringFromCertificate = (certificate) => { 144 | return certificate.notBefore.toSchema().toDate().toString(); 145 | } 146 | -------------------------------------------------------------------------------- /src/utils/throwif.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | exports.throwIf = (condition, message) => { 19 | if (condition) { 20 | throw new TypeError(message); 21 | } 22 | }; -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const isEqual = require('lodash.isequal'); 18 | 19 | /** 20 | * Return the mime type for the given `str`. 21 | * 22 | * @param {String} str 23 | * @return {String} 24 | * @api private 25 | */ 26 | 27 | exports.type = str => str.split(/ *; */).shift(); 28 | 29 | 30 | /** 31 | * Combine two ArrayBuffers. 32 | */ 33 | exports.appendBuffer = (buffer1, buffer2) => { 34 | var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); 35 | tmp.set(new Uint8Array(buffer1), 0); 36 | tmp.set(new Uint8Array(buffer2), buffer1.byteLength); 37 | return tmp.buffer; 38 | }; 39 | 40 | 41 | /** 42 | * 43 | */ 44 | exports.toUTF8Array = (str) => { 45 | var utf8 = []; 46 | for (var i=0; i < str.length; i++) { 47 | var charcode = str.charCodeAt(i); 48 | if (charcode < 0x80) utf8.push(charcode); 49 | else if (charcode < 0x800) { 50 | utf8.push(0xc0 | (charcode >> 6), 51 | 0x80 | (charcode & 0x3f)); 52 | } 53 | else if (charcode < 0xd800 || charcode >= 0xe000) { 54 | utf8.push(0xe0 | (charcode >> 12), 55 | 0x80 | ((charcode>>6) & 0x3f), 56 | 0x80 | (charcode & 0x3f)); 57 | } 58 | // surrogate pair 59 | else { 60 | i++; 61 | // UTF-16 encodes 0x10000-0x10FFFF by 62 | // subtracting 0x10000 and splitting the 63 | // 20 bits of 0x0-0xFFFFF into two halves 64 | charcode = 0x10000 + (((charcode & 0x3ff)<<10) 65 | | (str.charCodeAt(i) & 0x3ff)); 66 | utf8.push(0xf0 | (charcode >>18), 67 | 0x80 | ((charcode>>12) & 0x3f), 68 | 0x80 | ((charcode>>6) & 0x3f), 69 | 0x80 | (charcode & 0x3f)); 70 | } 71 | } 72 | return utf8; 73 | } 74 | 75 | 76 | /** 77 | * The base implementation of `sum` and `sumBy`. 78 | * 79 | * @private 80 | * @param {Array} array The array to iterate over. 81 | * @param {Function} iteratee The function invoked per iteration. 82 | * @returns {number} Returns the sum. 83 | */ 84 | exports.baseSum = (array, iteratee) => { 85 | let result 86 | 87 | for (const value of array) { 88 | const current = iteratee(value) 89 | if (current !== undefined) { 90 | result = result === undefined ? current : (result + current) 91 | } 92 | } 93 | return result 94 | } 95 | 96 | /** 97 | * This method is like `sum` except that it accepts `iteratee` which is 98 | * invoked for each element in `array` to generate the value to be summed. 99 | * The iteratee is invoked with one argument: (value). 100 | * 101 | * @since 4.0.0 102 | * @category Math 103 | * @param {Array} array The array to iterate over. 104 | * @param {Function} iteratee The iteratee invoked per element. 105 | * @returns {number} Returns the sum. 106 | * @example 107 | * 108 | * const objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }] 109 | * 110 | * sumBy(objects, ({ n }) => n) 111 | * // => 20 112 | */ 113 | exports.sumBy = (array, iteratee) => { 114 | return (array !== null && array.length) 115 | ? exports.baseSum(array, iteratee) 116 | : 0 117 | } 118 | 119 | 120 | /** 121 | * Parse a URL. 122 | * 123 | * @param Mixed url 124 | * @return Object 125 | */ 126 | exports.parseURL = (url) => { 127 | var parsedURL = new URL(url); 128 | var parser = document.createElement('a'), 129 | searchObject = {}, 130 | queries, split, i; 131 | 132 | // Let the browser do the work 133 | parser.href = url; 134 | 135 | // Convert query string to object 136 | queries = parser.search.replace(/^\?/, '').split('&'); 137 | for( i = 0; i < queries.length; i++ ) { 138 | split = queries[i].split('='); 139 | searchObject[split[0]] = split[1]; 140 | } 141 | 142 | let port = parser.port; 143 | if (isEqual(port, '')) { 144 | port = isEqual(parser.protocol, 'http:') ? 80 : 443; 145 | } 146 | 147 | return { 148 | protocol: parser.protocol, 149 | host: parser.host, 150 | hostname: parser.hostname, 151 | port: port, 152 | pathname: parser.pathname, 153 | search: parser.search, 154 | searchObject: searchObject, 155 | hash: parser.hash 156 | }; 157 | } 158 | 159 | exports.concatTypedArrays = (a, b) => { // a, b TypedArray of same type 160 | var c = new (a.constructor)(a.length + b.length); 161 | c.set(a, 0); 162 | c.set(b, a.length); 163 | return c; 164 | } 165 | -------------------------------------------------------------------------------- /src/utils/ziti-reporter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | class ZitiReporter { 19 | constructor (options) { 20 | this.options = Object.assign({}, options) 21 | 22 | this.defaultColor = '#7f8c8d' // Gray 23 | 24 | this.levelColorMap = { 25 | 0: '#f60404', // Red FATAL | ERROR 26 | 1: '#ff5c00', // Orange WARN 27 | 2: '#ffeb3b', // Yellow LOG 28 | 3: '#4caf50', // Green INFO 29 | 4: '#008efe', // Blue DEBUG 30 | 5: '#04e2fe', // Cyan TRACE 31 | } 32 | 33 | this.typeColorMap = { 34 | success: '#ee0450', // 35 | // success: '#2ecc71' // Green 36 | } 37 | } 38 | 39 | log (logObj) { 40 | const consoleLogFn = logObj.level < 1 41 | // eslint-disable-next-line no-console 42 | ? (console.__error || console.error) 43 | // eslint-disable-next-line no-console 44 | : logObj.level === 1 && console.warn ? (console.__warn || console.warn) : (console.__log || console.log) 45 | 46 | // Type 47 | const type = logObj.type !== 'log' ? logObj.type : '' 48 | 49 | // Tag 50 | const tag = logObj.tag ? logObj.tag : '' 51 | 52 | // Styles 53 | const color = this.typeColorMap[logObj.type] || this.levelColorMap[logObj.level] || this.defaultColor 54 | const successStyle = ` 55 | border-radius: 1.5em; 56 | color: white; 57 | font-weight: bold; 58 | padding: 10px 1.5em; 59 | background-image: linear-gradient(to bottom right, #0e61ed , #ee044f); 60 | ` 61 | const normalStyle = ` 62 | background: ${color}; 63 | border-radius: 1.5em; 64 | color: white; 65 | font-weight: bold; 66 | padding: 2px 0.5em; 67 | ` 68 | 69 | const style = logObj.type === 'success' ? successStyle : normalStyle 70 | 71 | const badge = logObj.type === 'success' ? `%cZiti` : `%cZiti-${[tag, type].filter(Boolean).join(':')}` 72 | 73 | // Log to the console 74 | if (typeof logObj.args[0] === 'string') { 75 | consoleLogFn( 76 | `${badge}%c ${logObj.args[0]}`, 77 | style, 78 | // Empty string as style resets to default console style 79 | '', 80 | ...logObj.args.slice(1) 81 | ) 82 | } else { 83 | consoleLogFn(badge, style, ...logObj.args) 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * Expose `ZitiReporter`. 90 | */ 91 | 92 | module.exports = ZitiReporter; 93 | -------------------------------------------------------------------------------- /src/websocket/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 18 | const RobustWebSocket = require('./robust-websocket'); 19 | 20 | /** 21 | * Default options. 22 | */ 23 | 24 | /** 25 | * @typedef {Object} Options 26 | * @property {Function} [createWebSocket=url => new WebSocket(url)] - custom function for WebSocket construction. 27 | * 28 | * @property {Function} [packMessage=noop] - packs message for sending. For example, `data => JSON.stringify(data)`. 29 | * 30 | * @property {Function} [unpackMessage=noop] - unpacks received message. For example, `data => JSON.parse(data)`. 31 | * 32 | * @property {Function} [attachRequestId=noop] - injects request id into data. 33 | * For example, `(data, requestId) => Object.assign({requestId}, data)`. 34 | * 35 | * @property {Function} [extractRequestId=noop] - extracts request id from received data. 36 | * For example, `data => data.requestId`. 37 | * 38 | * @property {Function} [extractMessageData=event => event.data] - extracts data from event object. 39 | * 40 | * @property {Number} timeout=0 - timeout for opening connection and sending messages. 41 | * 42 | * @property {Number} connectionTimeout=0 - special timeout for opening connection, by default equals to `timeout`. 43 | * 44 | */ 45 | 46 | module.exports = { 47 | /** 48 | * See {@link Options.createWebSocket} 49 | * 50 | * @param {String} url 51 | * @returns {WebSocket} 52 | */ 53 | // createWebSocket: url => new RobustWebSocket(url, null, { 54 | // timeout: 5000, // milliseconds to wait before a connection is considered to have timed out 55 | // shouldReconnect: function(event, ws) { 56 | // if (event.code === 1008 || event.code === 1011) return; // Do not reconnect on 1008 (HTTP 400 equivalent) and 1011 (HTTP 500 equivalent) 57 | // return Math.pow(2.0, ws.attempts) * 1000; // reconnect with exponential back-off 58 | // }, 59 | // automaticOpen: true, 60 | // ignoreConnectivityEvents: false 61 | // }), 62 | 63 | createWebSocket: url => { 64 | let ws; 65 | if (typeof window === 'undefined') { // if running from service-worker 66 | ws = new WebSocket( url ); 67 | } else { 68 | ws = new realWebSocket( url ); 69 | } 70 | return ws; 71 | }, 72 | 73 | /** 74 | * See {@link Options.packMessage} 75 | * 76 | * @param {*} data 77 | * @returns {String|ArrayBuffer|Blob} 78 | */ 79 | packMessage: null, 80 | 81 | /** 82 | * See {@link Options.unpackMessage} 83 | * 84 | * @param {String|ArrayBuffer|Blob} data 85 | * @returns {*} 86 | */ 87 | unpackMessage: null, 88 | 89 | /** 90 | * See {@link Options.attachRequestId} 91 | * 92 | * @param {*} data 93 | * @param {String|Number} requestId 94 | * @returns {*} 95 | */ 96 | attachRequestId: null, 97 | 98 | /** 99 | * See {@link Options.extractRequestId} 100 | * 101 | * @param {*} data 102 | * @returns {String|Number|undefined} 103 | */ 104 | extractRequestId: null, 105 | 106 | /** 107 | * See {@link Options.extractMessageData} 108 | * 109 | * @param {*} event 110 | * @returns {*} 111 | */ 112 | extractMessageData: event => event.data, 113 | 114 | /** 115 | * See {@link Options.timeout} 116 | */ 117 | timeout: 0, 118 | 119 | /** 120 | * See {@link Options.connectionTimeout} 121 | */ 122 | connectionTimeout: 0, 123 | 124 | /** 125 | * See {@link Options.ctx} 126 | */ 127 | ctx: null, 128 | }; -------------------------------------------------------------------------------- /src/websocket/requests.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2020 Netfoundry, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Class for manage pending requests. 19 | * @private 20 | */ 21 | 22 | const PromiseController = require('promise-controller'); 23 | const promiseFinally = require('promise.prototype.finally'); 24 | 25 | module.exports = class Requests { 26 | constructor() { 27 | this._items = new Map(); 28 | } 29 | 30 | /** 31 | * Creates new request and stores it in the list. 32 | * 33 | * @param {String|Number} requestId 34 | * @param {Function} fn 35 | * @param {Number} timeout 36 | * @returns {Promise} 37 | */ 38 | create(requestId, fn, timeout) { 39 | this._rejectExistingRequest(requestId); 40 | return this._createNewRequest(requestId, fn, timeout); 41 | } 42 | 43 | resolve(requestId, data) { 44 | if (requestId && this._items.has(requestId)) { 45 | this._items.get(requestId).resolve(data); 46 | } 47 | } 48 | 49 | rejectAll(error) { 50 | this._items.forEach(request => request.isPending ? request.reject(error) : null); 51 | } 52 | 53 | _rejectExistingRequest(requestId) { 54 | const existingRequest = this._items.get(requestId); 55 | if (existingRequest && existingRequest.isPending) { 56 | existingRequest.reject(new Error(`WebSocket request is replaced, id: ${requestId}`)); 57 | } 58 | } 59 | 60 | _createNewRequest(requestId, fn, timeout) { 61 | const request = new PromiseController({ 62 | timeout, 63 | timeoutReason: `WebSocket request was rejected by timeout (${timeout} ms). RequestId: ${requestId}` 64 | }); 65 | this._items.set(requestId, request); 66 | return promiseFinally(request.call(fn), () => this._deleteRequest(requestId, request)); 67 | } 68 | 69 | _deleteRequest(requestId, request) { 70 | // this check is important when request was replaced 71 | if (this._items.get(requestId) === request) { 72 | this._items.delete(requestId); 73 | } 74 | } 75 | }; -------------------------------------------------------------------------------- /src/websocket/robust-websocket.js: -------------------------------------------------------------------------------- 1 | 2 | (function(factory, global) { 3 | if (typeof define === 'function' && define.amd) { 4 | define(function() { 5 | return factory(global, navigator) 6 | }) 7 | } else if (typeof exports === 'object' && typeof module === 'object') { 8 | module.exports = factory(global, navigator) 9 | } else { 10 | // mock the navigator object when under test since `navigator.onLine` is read only 11 | global.RobustWebSocket = factory(global, typeof Mocha !== 'undefined' ? Mocha : navigator) 12 | } 13 | })(function(global, navigator) { 14 | 15 | var RobustWebSocket = function(url, protocols, userOptions) { 16 | var realWs = { close: function() {} }, 17 | connectTimeout, 18 | self = this, 19 | attempts = 0, 20 | reconnects = -1, 21 | reconnectWhenOnlineAgain = false, 22 | explicitlyClosed = false, 23 | pendingReconnect, 24 | opts = Object.assign({}, 25 | RobustWebSocket.defaultOptions, 26 | typeof userOptions === 'function' ? { shouldReconnect: userOptions } : userOptions 27 | ) 28 | 29 | if (typeof opts.timeout !== 'number') { 30 | throw new Error('timeout must be the number of milliseconds to timeout a connection attempt') 31 | } 32 | 33 | if (typeof opts.shouldReconnect !== 'function') { 34 | throw new Error('shouldReconnect must be a function that returns the number of milliseconds to wait for a reconnect attempt, or null or undefined to not reconnect.') 35 | } 36 | 37 | ['bufferedAmount', 'url', 'readyState', 'protocol', 'extensions'].forEach(function(readOnlyProp) { 38 | Object.defineProperty(self, readOnlyProp, { 39 | get: function() { return realWs[readOnlyProp] } 40 | }) 41 | }) 42 | 43 | function clearPendingReconnectIfNeeded() { 44 | if (pendingReconnect) { 45 | clearTimeout(pendingReconnect) 46 | pendingReconnect = null 47 | } 48 | } 49 | 50 | var ononline = function(event) { 51 | if (reconnectWhenOnlineAgain) { 52 | clearPendingReconnectIfNeeded() 53 | reconnect(event) 54 | } 55 | }, 56 | onoffline = function() { 57 | reconnectWhenOnlineAgain = true 58 | realWs.close(1000) 59 | }, 60 | connectivityEventsAttached = false 61 | 62 | function detachConnectivityEvents() { 63 | if (connectivityEventsAttached) { 64 | global.removeEventListener('online', ononline) 65 | global.removeEventListener('offline', onoffline) 66 | connectivityEventsAttached = false 67 | } 68 | } 69 | 70 | function attachConnectivityEvents() { 71 | if (!connectivityEventsAttached) { 72 | global.addEventListener('online', ononline) 73 | global.addEventListener('offline', onoffline) 74 | connectivityEventsAttached = true 75 | } 76 | } 77 | 78 | self.send = function() { 79 | return realWs.send.apply(realWs, arguments) 80 | } 81 | 82 | self.close = function(code, reason) { 83 | if (typeof code !== 'number') { 84 | reason = code 85 | code = 1000 86 | } 87 | 88 | clearPendingReconnectIfNeeded() 89 | reconnectWhenOnlineAgain = false 90 | explicitlyClosed = true 91 | detachConnectivityEvents() 92 | 93 | return realWs.close(code, reason) 94 | } 95 | 96 | self.open = function() { 97 | if (realWs.readyState !== WebSocket.OPEN && realWs.readyState !== WebSocket.CONNECTING) { 98 | clearPendingReconnectIfNeeded() 99 | reconnectWhenOnlineAgain = false 100 | explicitlyClosed = false 101 | 102 | newWebSocket() 103 | } 104 | } 105 | 106 | function reconnect(event) { 107 | if ((!opts.shouldReconnect.handle1000 && event.code === 1000) || explicitlyClosed) { 108 | attempts = 0 109 | return 110 | } 111 | if (navigator.onLine === false) { 112 | reconnectWhenOnlineAgain = true 113 | return 114 | } 115 | 116 | var delay = opts.shouldReconnect(event, self) 117 | if (typeof delay === 'number') { 118 | pendingReconnect = setTimeout(newWebSocket, delay) 119 | } 120 | } 121 | 122 | Object.defineProperty(self, 'listeners', { 123 | value: { 124 | open: [function(event) { 125 | if (connectTimeout) { 126 | clearTimeout(connectTimeout) 127 | connectTimeout = null 128 | } 129 | event.reconnects = ++reconnects 130 | event.attempts = attempts 131 | attempts = 0 132 | reconnectWhenOnlineAgain = false 133 | }], 134 | close: [reconnect] 135 | } 136 | }) 137 | 138 | Object.defineProperty(self, 'attempts', { 139 | get: function() { return attempts }, 140 | enumerable: true 141 | }) 142 | 143 | Object.defineProperty(self, 'reconnects', { 144 | get: function() { return reconnects }, 145 | enumerable: true 146 | }) 147 | 148 | function newWebSocket() { 149 | var newUrl = (typeof url === 'function' ? url(self) : url); 150 | pendingReconnect = null 151 | if (typeof window === 'undefined') { // if running from service-worker 152 | realWs = new WebSocket(newUrl, protocols || undefined) 153 | } else { 154 | realWs = new realWebSocket(newUrl, protocols || undefined) 155 | } 156 | realWs.binaryType = self.binaryType 157 | 158 | attempts++ 159 | self.dispatchEvent(Object.assign(new CustomEvent('connecting'), { 160 | attempts: attempts, 161 | reconnects: reconnects 162 | })) 163 | 164 | connectTimeout = setTimeout(function() { 165 | connectTimeout = null 166 | detachConnectivityEvents() 167 | self.dispatchEvent(Object.assign(new CustomEvent('timeout'), { 168 | attempts: attempts, 169 | reconnects: reconnects 170 | })) 171 | }, opts.timeout) 172 | 173 | ;['open', 'close', 'message', 'error'].forEach(function(stdEvent) { 174 | realWs.addEventListener(stdEvent, function(event) { 175 | self.dispatchEvent(event) 176 | 177 | var cb = self['on' + stdEvent] 178 | if (typeof cb === 'function') { 179 | return cb.apply(self, arguments) 180 | } 181 | }) 182 | }) 183 | 184 | if (!opts.ignoreConnectivityEvents) { 185 | attachConnectivityEvents() 186 | } 187 | } 188 | 189 | if (opts.automaticOpen) { 190 | newWebSocket() 191 | } 192 | } 193 | 194 | RobustWebSocket.defaultOptions = { 195 | // the time to wait before a successful connection 196 | // before the attempt is considered to have timed out 197 | timeout: 4000, 198 | // Given a CloseEvent or OnlineEvent and the RobustWebSocket state, 199 | // should a reconnect be attempted? Return the number of milliseconds to wait 200 | // to reconnect (or null or undefined to not), rather than true or false 201 | shouldReconnect: function(event, ws) { 202 | if (event.code === 1008 || event.code === 1011) return 203 | return [0, 3000, 10000][ws.attempts] 204 | }, 205 | 206 | // Flag to control whether attachement to navigator online/offline events 207 | // should be disabled. 208 | ignoreConnectivityEvents: false, 209 | 210 | // Create and connect the WebSocket when the instance is instantiated. 211 | // Defaults to true to match standard WebSocket behavior 212 | automaticOpen: true 213 | } 214 | 215 | RobustWebSocket.prototype.binaryType = 'blob' 216 | 217 | // Taken from MDN https://developer.mozilla.org/en-US/docs/Web/API/EventTarget 218 | RobustWebSocket.prototype.addEventListener = function(type, callback) { 219 | if (!(type in this.listeners)) { 220 | this.listeners[type] = [] 221 | } 222 | this.listeners[type].push(callback) 223 | } 224 | 225 | RobustWebSocket.prototype.removeEventListener = function(type, callback) { 226 | if (!(type in this.listeners)) { 227 | return 228 | } 229 | var stack = this.listeners[type] 230 | for (var i = 0, l = stack.length; i < l; i++) { 231 | if (stack[i] === callback) { 232 | stack.splice(i, 1) 233 | return 234 | } 235 | } 236 | } 237 | 238 | RobustWebSocket.prototype.dispatchEvent = function(event) { 239 | if (!(event.type in this.listeners)) { 240 | return 241 | } 242 | var stack = this.listeners[event.type] 243 | for (var i = 0, l = stack.length; i < l; i++) { 244 | stack[i].call(this, event) 245 | } 246 | } 247 | 248 | return RobustWebSocket 249 | }, typeof window != 'undefined' ? window : (typeof global != 'undefined' ? global : this)); 250 | -------------------------------------------------------------------------------- /tests/karma/client.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var ziti = require('../../src/index'); 3 | 4 | require('../mock-localstorage'); 5 | 6 | 7 | (function() { 8 | 9 | 10 | describe('client', function() { 11 | 12 | it('undefined context should fail', () => { 13 | assert.notEqual(ziti, undefined); 14 | assert.throws(function () { 15 | ziti.newConnection(undefined, null); 16 | }, /^TypeError: Specified context is undefined.$/); 17 | 18 | 19 | }); 20 | 21 | it('null context should fail', () => { 22 | assert.notEqual(ziti, undefined); 23 | assert.throws(function () { 24 | ziti.newConnection(null, null); 25 | }, /^TypeError: Specified context is null.$/); 26 | }); 27 | 28 | it('ziti.init should succeed', async () => { 29 | let ctx = await ziti.init(); 30 | assert.notEqual(ctx, undefined); 31 | }); 32 | 33 | it('ziti.init should succeed', async () => { 34 | let ctx = await ziti.init(); 35 | assert.notEqual(ctx, undefined); 36 | let conn = ziti.newConnection(ctx, null); 37 | assert.notEqual(conn, undefined); 38 | // console.log('conn is: ', conn); 39 | }); 40 | 41 | }); 42 | 43 | })(); 44 | -------------------------------------------------------------------------------- /tests/karma/index.js: -------------------------------------------------------------------------------- 1 | // The karma tests include all standard unit tests... 2 | require('../unit'); 3 | 4 | // Items below require a browser, so are only run under Karma 5 | require('./utils'); 6 | require('./client'); 7 | -------------------------------------------------------------------------------- /tests/karma/utils.js: -------------------------------------------------------------------------------- 1 | var ASSERT = require('assert'); 2 | var UTILS = require('../../src/utils/utils'); 3 | 4 | 5 | (function() { 6 | 7 | // custom assertion to test array-like objects 8 | function assertArrayEqual(actual, expected) { 9 | ASSERT.equal(actual.length, expected.length); 10 | for(var idx = 0; idx < expected.length; idx++) { 11 | ASSERT.equal(actual[idx], expected[idx]); 12 | } 13 | } 14 | 15 | describe('util', function() { 16 | 17 | it('should parse a URL', function() { 18 | var parsedUrl = UTILS.parseURL( 'https://somewhere.ziti:1234/the/path?foo=bar' ); 19 | ASSERT.equal(parsedUrl.protocol, 'https:'); 20 | ASSERT.equal(parsedUrl.host, 'somewhere.ziti:1234'); 21 | ASSERT.equal(parsedUrl.hostname, 'somewhere.ziti'); 22 | ASSERT.equal(parsedUrl.port, 1234); 23 | ASSERT.equal(parsedUrl.pathname, '/the/path'); 24 | ASSERT.equal(parsedUrl.search, '?foo=bar'); 25 | }); 26 | 27 | }); 28 | 29 | })(); 30 | -------------------------------------------------------------------------------- /tests/mock-localstorage.js: -------------------------------------------------------------------------------- 1 | // global.window = {} 2 | // require('mock-local-storage'); 3 | // window.localStorage = global.localStorage 4 | 5 | 6 | const item = { 7 | value: "https://curt-edge-controller:1280", 8 | expiry: 1629296626000, 9 | } 10 | localStorage.setItem("ZITI_CONTROLLER", JSON.stringify(item)) 11 | -------------------------------------------------------------------------------- /tests/unit/index.js: -------------------------------------------------------------------------------- 1 | require('./utils'); 2 | -------------------------------------------------------------------------------- /tests/unit/utils.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var UTILS = require('../../src/utils/utils'); 3 | 4 | 5 | (function() { 6 | 7 | // custom assertion to test array-like objects 8 | function assertArrayEqual(actual, expected) { 9 | assert.equal(actual.length, expected.length); 10 | for(var idx = 0; idx < expected.length; idx++) { 11 | assert.equal(actual[idx], expected[idx]); 12 | } 13 | } 14 | 15 | describe('util', function() { 16 | 17 | it('should succeed', () => { 18 | assert.ok(true); 19 | }); 20 | 21 | 22 | it('should append buffers', function() { 23 | var buffer1 = Buffer.from('1234', 'utf8'); 24 | var buffer2 = Buffer.from('5678', 'utf8'); 25 | var buffer3 = UTILS.appendBuffer(buffer1, buffer2); 26 | assert.equal(buffer3.byteLength, 8); 27 | }); 28 | 29 | 30 | it('should create utf8 array', function() { 31 | var utf8array = UTILS.toUTF8Array('0123456789'); 32 | assert.notEqual(utf8array, undefined); 33 | assert.equal(utf8array[0], 48); 34 | assert.equal(utf8array[1], 49); 35 | assert.equal(utf8array[2], 50); 36 | assert.equal(utf8array[3], 51); 37 | assert.equal(utf8array[4], 52); 38 | assert.equal(utf8array[5], 53); 39 | assert.equal(utf8array[6], 54); 40 | assert.equal(utf8array[7], 55); 41 | assert.equal(utf8array[8], 56); 42 | assert.equal(utf8array[9], 57); 43 | }); 44 | 45 | 46 | it('should sum array properties', function() { 47 | const objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }] 48 | var number = UTILS.sumBy(objects, ({ n }) => n); 49 | assert.equal(number, 20); 50 | }); 51 | 52 | }); 53 | 54 | })(); 55 | --------------------------------------------------------------------------------