├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── mattermost-ziti-webhook.yml │ └── promote-to-stable.yml ├── .gitignore ├── .greenlockrc ├── .yarn └── install-state.gz ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── canny-setup.js ├── favicon.ico ├── layout.css ├── template-zbr.ejs ├── template-zrok.ejs └── template.ejs ├── config.json ├── greenlock.d └── config.json ├── index.js ├── lib ├── edge │ ├── constants.js │ ├── context-options.js │ ├── context.js │ ├── fetch-polyfill.js │ └── flat-options.js ├── env.js ├── http-proxy.js ├── http-proxy │ ├── common.js │ ├── index.js │ └── passes │ │ ├── cors-proxy.js │ │ ├── urlon.js │ │ ├── web-incoming-zitified.js │ │ ├── ziti-request.js │ │ └── ziti-response.js ├── inject.js ├── oidc │ └── utils.js ├── terminate.js └── white-list-filter.js ├── package.json ├── yarn.lock └── zha-docker-entrypoint /.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore local assets from development 2 | node_modules 3 | -------------------------------------------------------------------------------- /.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 | 2 | name: Build 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | paths-ignore: 8 | - 'package.json' 9 | - 'CHANGELOG.md' 10 | pull_request: 11 | branches: [ main ] 12 | workflow_dispatch: 13 | inputs: 14 | tags: 15 | required: false 16 | description: 'Misc tags' 17 | 18 | jobs: 19 | build: 20 | name: 'Build' 21 | runs-on: ubuntu-latest 22 | env: 23 | BUILD_NUMBER: ${{ github.run_number }} 24 | TARGET_PLATFORMS: linux/amd64,linux/arm64 25 | 26 | steps: 27 | 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Enable Corepack 32 | run: corepack enable 33 | 34 | - name: Prepare Yarn 4 35 | run: corepack prepare yarn@4.0.2 --activate 36 | 37 | - name: Verify Yarn version 38 | run: yarn -v 39 | 40 | - name: Set up Node.js with Corepack 41 | uses: actions/setup-node@v4 42 | with: 43 | node-version: 22 # Or another supported version 44 | 45 | - name: Set up Docker Buildx 46 | uses: docker/setup-buildx-action@v1 47 | 48 | - name: Login to GitHub Container Registry 49 | uses: docker/login-action@v1 50 | with: 51 | registry: ghcr.io 52 | username: ${{ secrets.GHCR_USER }} 53 | password: ${{ secrets.GHCR_PAT }} 54 | 55 | - name: Extract branch name 56 | shell: bash 57 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 58 | id: extract_branch 59 | 60 | - name: Extract PR number 61 | shell: bash 62 | run: | 63 | echo "##[set-output name=pr_number;]$(echo $(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH"))" 64 | id: extract_pr_number 65 | 66 | - name: Extract version number 67 | shell: bash 68 | run: | 69 | echo "##[set-output name=version_number;]$(echo $(jq -r .version package.json))" 70 | id: extract_version_number 71 | 72 | - name: Build/Push (main) 73 | uses: docker/build-push-action@v2 74 | with: 75 | context: . 76 | push: true 77 | platforms: ${{ env.TARGET_PLATFORMS }} 78 | tags: | 79 | ghcr.io/openziti/ziti-browzer-bootstrapper:${{ github.run_number }} 80 | ghcr.io/openziti/ziti-browzer-bootstrapper:latest 81 | ghcr.io/openziti/ziti-browzer-bootstrapper:${{ steps.extract_version_number.outputs.version_number }} 82 | if: | 83 | steps.extract_branch.outputs.branch == 'main' 84 | 85 | - name: Build/Push (PR) 86 | uses: docker/build-push-action@v2 87 | with: 88 | context: . 89 | push: true 90 | platforms: ${{ env.TARGET_PLATFORMS }} 91 | tags: | 92 | ghcr.io/openziti/ziti-browzer-bootstrapper:${{ github.run_number }} 93 | ghcr.io/openziti/ziti-browzer-bootstrapper:PR${{ steps.extract_pr_number.outputs.pr_number }}.${{ github.run_number }} 94 | if: | 95 | steps.extract_branch.outputs.branch != 'main' 96 | -------------------------------------------------------------------------------- /.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 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '40 9 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.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: ubuntu-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 | -------------------------------------------------------------------------------- /.github/workflows/promote-to-stable.yml: -------------------------------------------------------------------------------- 1 | name: Promote Image to Stable 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Image tag to promote to stable" 8 | required: true 9 | push: 10 | branches: 11 | - bootstrapper-issue-364 12 | 13 | jobs: 14 | promote: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Login to GitHub Container Registry 19 | uses: docker/login-action@v1 20 | with: 21 | registry: ghcr.io 22 | username: ${{ secrets.GHCR_USER }} 23 | password: ${{ secrets.GHCR_PAT }} 24 | 25 | - name: Retag image as stable 26 | run: | 27 | IMAGE_NAME=ghcr.io/${{ github.repository_owner }}/ziti-browzer-bootstrapper 28 | VERSION=${{ github.event.inputs.version }} 29 | 30 | # Pull the existing image 31 | docker pull $IMAGE_NAME:$VERSION 32 | 33 | # Retag it as "stable" 34 | docker tag $IMAGE_NAME:$VERSION $IMAGE_NAME:stable 35 | 36 | # Push both tags 37 | docker push $IMAGE_NAME:$VERSION 38 | docker push $IMAGE_NAME:stable 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | *-audit.json 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | .vscode/ 108 | .clinic/ 109 | .DS_Store 110 | 111 | docker-compose.override.yml 112 | .env 113 | .idea 114 | 115 | start-*.sh -------------------------------------------------------------------------------- /.greenlockrc: -------------------------------------------------------------------------------- 1 | {"manager":{"module":"@greenlock/manager"},"configDir":"./greenlock.d"} -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openziti/ziti-browzer-bootstrapper/659ece7306dd95c09f4d21da934eacb8dde7e679/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | 131 | -------------------------------------------------------------------------------- /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://netfoundry.github.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 | 8 | **Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Install dependencies 2 | FROM node:22-slim AS build 3 | 4 | LABEL maintainer="OpenZiti " 5 | 6 | # Install useful tools 7 | RUN apt-get update 8 | RUN apt-get install -y python3 build-essential curl 9 | 10 | # Create directory for the Ziti BrowZer Bootstrapper, and explicitly set the owner of that new directory to the node user 11 | RUN mkdir /home/node/ziti-browzer-bootstrapper 12 | WORKDIR /home/node/ziti-browzer-bootstrapper 13 | 14 | # Prepare for dependencies installation 15 | COPY --chown=node:node package.json ./ 16 | COPY --chown=node:node yarn.lock ./ 17 | 18 | # Install Yarn 4 globally 19 | RUN corepack enable && corepack prepare yarn@4.0.2 --activate && yarn config set nodeLinker node-modules 20 | 21 | # Bring in the source of the Ziti BrowZer Bootstrapper to the working folder 22 | COPY --chown=node:node index.js . 23 | COPY --chown=node:node zha-docker-entrypoint . 24 | COPY --chown=node:node lib ./lib/ 25 | COPY --chown=node:node assets ./assets/ 26 | 27 | # Install dependencies (ensuring node_modules remains) 28 | RUN yarn install 29 | 30 | RUN ls -l 31 | 32 | # Stage 2: Production-ready image 33 | FROM node:22-slim 34 | 35 | RUN apt-get update && apt-get install -y curl 36 | 37 | WORKDIR /home/node/ziti-browzer-bootstrapper 38 | 39 | # Copy installed node_modules from build stage 40 | COPY --from=build /home/node/ziti-browzer-bootstrapper /home/node/ziti-browzer-bootstrapper 41 | 42 | RUN chown -R node:node /home/node/ziti-browzer-bootstrapper 43 | USER node 44 | 45 | # Expose the Ziti BrowZer Bootstrapper for traffic to be proxied (8000) and the 46 | # REST API where it can be configured (8001) 47 | EXPOSE 8000 48 | EXPOSE 8443 49 | 50 | # Put the Ziti BrowZer Bootstrapper on path for zha-docker-entrypoint 51 | ENV PATH=/home/node/bin:$PATH 52 | ENTRYPOINT ["/home/node/ziti-browzer-bootstrapper/zha-docker-entrypoint"] 53 | 54 | # CMD ["node index.js > ./log/ziti-browzer-bootstrapper.log > 2&1"] 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `ziti-browzer-bootstrapper` 2 | ===================== 3 | 4 | A NodeJS-based server responsible for securely bootstrapping browser-based web applications over an 5 | [OpenZiti Overlay Network](https://openziti.io/docs/reference/glossary/#network-overlay-overlay) 6 | 7 | 8 | 9 | Learn about OpenZiti at [openziti.io](https://openziti.io) 10 | 11 | 12 | [![Build](https://github.com/openziti/ziti-browzer-bootstrapper/workflows/Build/badge.svg?branch=main)]() 13 | [![Issues](https://img.shields.io/github/issues-raw/openziti/ziti-browzer-bootstrapper)]() 14 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 15 | [![LOC](https://img.shields.io/tokei/lines/github/openziti/ziti-browzer-bootstrapper)]() 16 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=rounded)](CONTRIBUTING.md) 17 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) 18 | 19 | 20 | 21 | 22 | 23 | - [Motivation](#motivation) 24 | - [Features](#features) 25 | - [Installing/Running](#installingrunning) 26 | - [Configuration](#configuration) 27 | - [License](#license) 28 | 29 | 30 | 31 | 32 | ## Motivation 33 | 34 | Zero trust is an evolving landscape. Lots of enterprises, organizations, individuals would like to have a more robust 35 | security posture, but installing agents is sometimes a non-starter. BrowZer bypasses the need for installing agents on 36 | devices that are accessing resources secured by the OpenZiti overlay network. Instead of relying on a client to be 37 | installed, BrowZer enables "clientless zero trust". Users do not need to install any client to use an http-based resource 38 | protected by BrowZer and OpenZiti 39 | 40 | ## Features 41 | 42 | Client-less zero trust, bootstrapped entirely in the browser! 43 | 44 | ## Prerequisites 45 | 46 | ### Node/Yarn 47 | 48 | The project relies on NodeJS. You'll obviously need to have node available. The project also relies on [yarn](https://yarnpkg.com/) 49 | for building. Ensure you have the necessary version of Node and Yarn installed 50 | 51 | ### OpenZiti Network 52 | 53 | To run the project, you'll first need to have administrator access to a running OpenZiti overlay. Follow [one of the 54 | network quickstarts](https://openziti.io/docs/learn/quickstarts/network/) to get an overlay network running. Ensure 55 | you have configured the overlay with "alternative server certs" as outlined 56 | [in the documentation](https://openziti.io/docs/guides/alt-server-certs). 57 | 58 | ### third-party verifiable, wildcard certificate 59 | BrowZer operates in your browser, having a PKI that is not self-signed make using BrowZer much easier. LetsEncrypt 60 | makes obtaining certificates attainable for nearly everyone. It is easier to procure a wildcard certificate and use it 61 | for not only your OpenZiti overlay network, but also for the ziti-browzer-bootstrapper as well. 62 | 63 | ### OIDC Provider 64 | 65 | BrowZer leverages OpenZiti's "ext-jwt-signers" functionality. This functionality allows delegation of authentication to 66 | configured OIDC providers. To use BrowZer you will **need** and OIDC provider. There are many providers available to 67 | choose from. Find one that works for you. 68 | 69 | ## Configuring the OpenZiti Network 70 | 71 | To configure the OpenZiti overlay, you'll need to do the following: 72 | 73 | * create a valid `ext-jwt-signer` 74 | * create an `auth-policy` that uses the configured `ext-jwt-signer` 75 | * associate the `auth-policy` to the identities which are to be enabled for BrowZer-based authentication 76 | 77 | [BrowZer IdP configuration guides](https://openziti.io/docs/identity-providers-for-browZer) 78 | 79 | ### Create a valid ext-jwt-signer 80 | 81 | The easiest way to configure the ext-jwt-signer will be to use the OIDC discovery endpoint. Most OIDC providers will expose 82 | this endpoint to you. The URL will generally end with `.well-known/openid-configuration`. For example, if you use Auth0 83 | as your OIDC provider you'll be given a 'domain' from Auth0 that will look like: https://dev-blah_blah_xctngxka.us.auth0.com. 84 | For this Auth0 domain, the discovery endpoint will be at `https://dev-blah_blah_xctngxka.us.auth0.com/.well-known/openid-configuration`. 85 | Inspecting this endpoint will provide you with the information you need to configure the overlay. All the OIDC providers 86 | will provide a CLIENT_ID of some kind. You will also need to know this value to configure BrowZer and OpenZiti properly. 87 | There's lots of information about the client id on the internet, one such source you can use to read about client id 88 | [is provided here](https://www.oauth.com/oauth2-servers/client-registration/client-id-secret/) 89 | 90 | Another piece of information you will require from the OIDC provider is the expected claim value to associate the user to. Every OIDC provider 91 | is different. It's up to you to understand what claim you want to map. As another example, staying with Auth0, you 92 | will see the JWT bearer token returned from Auth0 will contain a claim named "email". When creating the `ext-jwt-signer`, 93 | this claim is referenced as the 'claims-property'. 94 | 95 | Here's a very simple set of steps that illustrates how you might use the `ziti` CLI with Auth0 to create an external 96 | jwt signer in your OpenZiti overlay (see the official doc site for more information) This example will not work for you as-is, 97 | you'll need to supply the proper inputs. The example is for illustration only: 98 | 99 | ```bash 100 | ZITI_BROWZER_OIDC_URL=https://dev-blah_blah_xctngxka.us.auth0.com 101 | ZITI_BROWZER_CLIENT_ID=blah_blah_45p8tvLJTbRbGw6TU2xjj 102 | ZITI_BROWZER_FIELD=email 103 | issuer=$(curl -s ${ZITI_BROWZER_OIDC_URL}/.well-known/openid-configuration | jq -r .issuer) 104 | jwks=$(curl -s ${ZITI_BROWZER_OIDC_URL}/.well-known/openid-configuration | jq -r .jwks_uri) 105 | 106 | echo "OIDC issuer : $issuer" 107 | echo "OIDC jwks url : $jwks" 108 | 109 | ext_jwt_signer=$(ziti edge create ext-jwt-signer "browzer-auth0-ext-jwt-signer" "${issuer}" --jwks-endpoint "${jwks}" --audience "${ZITI_BROWZER_CLIENT_ID}" --claims-property ${ZITI_BROWZER_FIELD}) 110 | echo "ext jwt signer id: $ext_jwt_signer" 111 | ``` 112 | 113 | ### Create an Authentication Policy 114 | 115 | Once the external jwt signer is created you will need an [authentication policy](https://openziti.io/docs/learn/core-concepts/security/authentication/authentication-policies). 116 | If you have run the command above, you will be able to create and echo the auth-policy using a command similar to this 117 | one (see the official doc site for more information): 118 | 119 | ```bash 120 | auth_policy=$(ziti edge create auth-policy browzer-auth0-auth-policy --primary-ext-jwt-allowed --primary-ext-jwt-allowed-signers ${ext_jwt_signer}) 121 | echo "auth policy id: $auth_policy" 122 | ``` 123 | 124 | ### Authorizing an Identity for BrowZer 125 | 126 | Once the external signer and auth policy are in place, now you need to associate the policy with an identity. 127 | For example, if you ran the auth policy command shown above, you could do something as shown: 128 | 129 | ```bash 130 | id=some.email@address.ziti 131 | ziti edge create identity user "${id}" --auth-policy ${auth_policy} --external-id "${id}" -a browzer.enabled.identities 132 | ``` 133 | 134 | This creates an association in OpenZiti mapping an identity with "some.email.@address.ziti" to this OpenZiti identity. 135 | Continuing with Auth0 as the OIDC provider, when a user tries to use a service protected with BrowZer, after authenticating 136 | to Auth0, Auth0 is expected to return a bearer token with a field named email, containing "some.email@address.ziti". (the 137 | _actual_ email of the user should be returned, of course). If that's the case, now this user will be authorized to access 138 | the service. 139 | 140 | ## Installing/Running 141 | 142 | Once the prerequisites are met, starting the `ziti-browzer-bootstrapper` should be relatively simple. To start the bootstrapper you will 143 | be expected to provide the following environment variables before starting the service. If you are running with NodeJS, 144 | you'll set these as environment variables. If you're running via docker, you can either use a .env file or you can set 145 | environment variables. 146 | 147 | ### Environment Variables 148 | 149 | * NODE_ENV: controls if the environment is production or development 150 | * ZITI_BROWZER_RUNTIME_LOGLEVEL: the log level for the Ziti BrowZer Runtime (ZBR) to use 151 | * ZITI_CONTROLLER_HOST: the "alternative" address for the OpenZiti controller 152 | * ZITI_CONTROLLER_PORT: the port to find the OpenZiti controller at 153 | * ZITI_BROWZER_BOOTSTRAPPER_LOGLEVEL: the log level for the ziti-browzer-bootstrapper to log at 154 | * ZITI_BROWZER_BOOTSTRAPPER_HOST: the address the ziti-browzer-bootstrapper is available at 155 | * ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT: the port the ziti-browzer-bootstrapper is available at 156 | * ZITI_BROWZER_BOOTSTRAPPER_SCHEME: the scheme to use to access the ziti-browzer-bootstrapper (https by default) 157 | * ZITI_BROWZER_BOOTSTRAPPER_CERTIFICATE_PATH: the path to the certificate the ziti-browzer-bootstrapper presents to clients 158 | * ZITI_BROWZER_BOOTSTRAPPER_KEY_PATH: the associated key for the ZITI_BROWZER_BOOTSTRAPPER_CERTIFICATE_PATH 159 | * ZITI_BROWZER_LOAD_BALANCER_HOST: the address of the load balancer (if an optional LB does TLS-termination in front of the ziti-browzer-bootstrapper) 160 | * ZITI_BROWZER_LOAD_BALANCER_PORT: the port the load balancer listens on (443 by default) 161 | * ZITI_BROWZER_BOOTSTRAPPER_TARGETS: __more on this below__ 162 | 163 | ```bash 164 | NODE_ENV: production 165 | ZITI_BROWZER_RUNTIME_LOGLEVEL: debug 166 | ZITI_BROWZER_RUNTIME_HOTKEY: alt+F12 167 | ZITI_CONTROLLER_HOST: ${ZITI_CTRL_EDGE_ALT_ADVERTISED_ADDRESS} 168 | ZITI_CONTROLLER_PORT: ${ZITI_CTRL_EDGE_ADVERTISED_PORT} 169 | ZITI_BROWZER_BOOTSTRAPPER_LOGLEVEL: debug 170 | ZITI_BROWZER_BOOTSTRAPPER_HOST: ${ZITI_BROWZER_BOOTSTRAPPER_HOST} 171 | ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT: ${ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT} 172 | ZITI_BROWZER_BOOTSTRAPPER_SCHEME: https 173 | ZITI_BROWZER_BOOTSTRAPPER_CERTIFICATE_PATH: /etc/letsencrypt/live/your.fqdn.here/fullchain.pem 174 | ZITI_BROWZER_BOOTSTRAPPER_KEY_PATH: /etc/letsencrypt/live/your.fqdn.here/privkey.pem 175 | ZITI_BROWZER_BOOTSTRAPPER_TARGETS: __more on this below__ 176 | ``` 177 | 178 | ### ZITI_BROWZER_BOOTSTRAPPER_TARGETS 179 | 180 | The `ZITI_BROWZER_BOOTSTRAPPER_TARGETS` environment variable is a json block that specifies the configuration of services the `ziti-browzer-bootstrapper` 181 | should support. The json is a single entry named "targetArray" which is an array of services to configure with one entry 182 | per service. An example json block would look like the following: 183 | ```json 184 | { 185 | "targetArray": [ 186 | { 187 | "vhost": "${ZITI_BROWZER_VHOST}", 188 | "service": "${ZITI_BROWZER_SERVICE}", 189 | "path": "/", 190 | "scheme": "http", 191 | "idp_issuer_base_url": "${ZITI_BROWZER_OIDC_URL}", 192 | "idp_client_id": "${ZITI_BROWZER_CLIENT_ID}" 193 | } 194 | ] 195 | } 196 | ``` 197 | 198 | ### Starting the ziti-browzer-bootstrapper 199 | 200 | Once you have set the required environment variables you can start the `ziti-browzer-bootstrapper` directly by running `yarn build` 201 | and then running: 202 | ```bash 203 | NODE_EXTRA_CA_CERTS=node_modules/node_extra_ca_certs_mozilla_bundle/ca_bundle/ca_intermediate_root_bundle.pem node index.js 204 | ``` 205 | 206 | To start the `ziti-browzer-bootstrapper` from docker you can issue a command, using the environment variables. For example: 207 | ```bash 208 | docker run 209 | --name ziti-browzer-bootstrapper 210 | --rm -v /etc/letsencrypt:/etc/letsencrypt 211 | --user 1000:2171 212 | -p 1443:1443 213 | -e NODE_ENV=production 214 | -e ZITI_BROWZER_BOOTSTRAPPER_LOGLEVEL=debug 215 | -e ZITI_BROWZER_RUNTIME_LOGLEVEL=debug 216 | -e ZITI_BROWZER_RUNTIME_HOTKEY=alt+F12 217 | -e ZITI_CONTROLLER_HOST=ctrl.zititv.demo.openziti.org 218 | -e ZITI_CONTROLLER_PORT=1280 219 | -e ZITI_BROWZER_BOOTSTRAPPER_HOST=browzer.zititv.demo.openziti.org 220 | -e ZITI_BROWZER_BOOTSTRAPPER_SCHEME=https 221 | -e ZITI_BROWZER_BOOTSTRAPPER_CERTIFICATE_PATH=/etc/letsencrypt/live/zititv.demo.openziti.org/fullchain.pem 222 | -e ZITI_BROWZER_BOOTSTRAPPER_KEY_PATH=/etc/letsencrypt/live/zititv.demo.openziti.org/privkey.pem 223 | -e ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT=1443 224 | -e ZITI_BROWZER_BOOTSTRAPPER_TARGETS= { 225 | "targetArray": [ 226 | { 227 | "vhost": "docker-whale.zititv.demo.openziti.org", 228 | "service": "docker.whale", 229 | "path": "/", 230 | "scheme": "http", 231 | "idp_issuer_base_url": "https://dev-b2q0t23rxctngxka.us.auth0.com", 232 | "idp_client_id": "Yo1JXbaLhp045p8tvLJTbRbGw6TU2xjj" 233 | } 234 | ] 235 | }\ 236 | ghcr.io/openziti/ziti-browzer-bootstrapper:pr177.432 237 | ``` 238 | 239 | 240 | [npm-image]: https://flat.badgen.net/npm/v/@openziti/ziti-sdk-js 241 | [npm-url]: https://www.npmjs.com/package/@openziti/ziti-sdk-js 242 | [install-size-image]: https://flat.badgen.net/packagephobia/install/@openziti/ziti-sdk-js 243 | [install-size-url]: https://packagephobia.now.sh/result?p=@openziti/ziti-sdk-js 244 | -------------------------------------------------------------------------------- /assets/canny-setup.js: -------------------------------------------------------------------------------- 1 | !function(w,d,i,s){function l(){if(!d.getElementById(i)){var f=d.getElementsByTagName(s)[0],e=d.createElement(s);e.type="text/javascript",e.async=!0,e.src="https://canny.io/sdk.js",f.parentNode.insertBefore(e,f)}}if("function"!=typeof w.Canny){var c=function(){c.q.push(arguments)};c.q=[],w.Canny=c,"complete"===d.readyState?l():w.attachEvent?w.attachEvent("onload",l):w.addEventListener("load",l,!1)}}(window,document,"canny-jssdk","script"); -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openziti/ziti-browzer-bootstrapper/659ece7306dd95c09f4d21da934eacb8dde7e679/assets/favicon.ico -------------------------------------------------------------------------------- /assets/layout.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | html { 4 | font-family: sans-serif; 5 | line-height: 1.15; 6 | -ms-text-size-adjust: 100%; 7 | -webkit-text-size-adjust: 100% 8 | } 9 | 10 | body { 11 | margin: 0 12 | } 13 | 14 | article,aside,footer,header,nav,section { 15 | display: block 16 | } 17 | 18 | h1 { 19 | font-size: 2em; 20 | margin: .67em 0 21 | } 22 | 23 | figcaption,figure,main { 24 | display: block 25 | } 26 | 27 | figure { 28 | margin: 1em 40px 29 | } 30 | 31 | hr { 32 | box-sizing: content-box; 33 | height: 0; 34 | overflow: visible 35 | } 36 | 37 | pre { 38 | font-family: monospace,monospace; 39 | font-size: 1em 40 | } 41 | 42 | a { 43 | background-color: transparent; 44 | -webkit-text-decoration-skip: objects 45 | } 46 | 47 | a:active,a:hover { 48 | outline-width: 0 49 | } 50 | 51 | abbr[title] { 52 | border-bottom: none; 53 | text-decoration: underline; 54 | text-decoration: underline dotted 55 | } 56 | 57 | b,strong { 58 | font-weight: inherit 59 | } 60 | 61 | b,strong { 62 | font-weight: bolder 63 | } 64 | 65 | code,kbd,samp { 66 | font-family: monospace,monospace; 67 | font-size: 1em 68 | } 69 | 70 | dfn { 71 | font-style: italic 72 | } 73 | 74 | mark { 75 | background-color: #ff0; 76 | color: #000 77 | } 78 | 79 | small { 80 | font-size: 80% 81 | } 82 | 83 | sub,sup { 84 | font-size: 75%; 85 | line-height: 0; 86 | position: relative; 87 | vertical-align: baseline 88 | } 89 | 90 | sub { 91 | bottom: -.25em 92 | } 93 | 94 | sup { 95 | top: -.5em 96 | } 97 | 98 | audio,video { 99 | display: inline-block 100 | } 101 | 102 | audio:not([controls]) { 103 | display: none; 104 | height: 0 105 | } 106 | 107 | img { 108 | border-style: none 109 | } 110 | 111 | svg:not(:root) { 112 | overflow: hidden 113 | } 114 | 115 | button,input,optgroup,select,textarea { 116 | font-family: sans-serif; 117 | font-size: 100%; 118 | line-height: 1.15; 119 | margin: 0 120 | } 121 | 122 | button,input { 123 | overflow: visible 124 | } 125 | 126 | button,select { 127 | text-transform: none 128 | } 129 | 130 | [type=reset],[type=submit],button,html [type=button] { 131 | -webkit-appearance: button 132 | } 133 | 134 | [type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner { 135 | border-style: none; 136 | padding: 0 137 | } 138 | 139 | [type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring { 140 | outline: 1px dotted ButtonText 141 | } 142 | 143 | fieldset { 144 | border: 1px solid silver; 145 | margin: 0 2px; 146 | padding: .35em .625em .75em 147 | } 148 | 149 | legend { 150 | box-sizing: border-box; 151 | color: inherit; 152 | display: table; 153 | max-width: 100%; 154 | padding: 0; 155 | white-space: normal 156 | } 157 | 158 | progress { 159 | display: inline-block; 160 | vertical-align: baseline 161 | } 162 | 163 | textarea { 164 | overflow: auto 165 | } 166 | 167 | [type=checkbox],[type=radio] { 168 | box-sizing: border-box; 169 | padding: 0 170 | } 171 | 172 | [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button { 173 | height: auto 174 | } 175 | 176 | [type=search] { 177 | -webkit-appearance: textfield; 178 | outline-offset: -2px 179 | } 180 | 181 | [type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration { 182 | -webkit-appearance: none 183 | } 184 | 185 | ::-webkit-file-upload-button { 186 | -webkit-appearance: button; 187 | font: inherit 188 | } 189 | 190 | details,menu { 191 | display: block 192 | } 193 | 194 | summary { 195 | display: list-item 196 | } 197 | 198 | canvas { 199 | display: inline-block 200 | } 201 | 202 | template { 203 | display: none 204 | } 205 | 206 | [hidden] { 207 | display: none 208 | } 209 | 210 | /*! Simple HttpErrorPages | MIT X11 License | https://github.com/AndiDittrich/HttpErrorPages */ 211 | 212 | body,html { 213 | width: 100%; 214 | height: 100%; 215 | background-color: #21232a 216 | } 217 | 218 | body { 219 | color: #fff; 220 | text-align: center; 221 | text-shadow: 0 2px 4px rgba(0,0,0,.5); 222 | padding: 0; 223 | min-height: 100%; 224 | -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.8); 225 | box-shadow: inset 0 0 100px rgba(0,0,0,.8); 226 | display: table; 227 | font-family: "Open Sans",Arial,sans-serif 228 | } 229 | 230 | h1 { 231 | font-family: inherit; 232 | font-weight: 500; 233 | line-height: 1.1; 234 | color: inherit; 235 | font-size: 30px 236 | } 237 | 238 | h1 small { 239 | font-size: 68%; 240 | font-weight: 400; 241 | line-height: 1; 242 | color: #777 243 | } 244 | 245 | a { 246 | text-decoration: none; 247 | color: #fff; 248 | font-size: inherit; 249 | border-bottom: dotted 1px #707070 250 | } 251 | 252 | .lead { 253 | color: silver; 254 | font-size: 21px; 255 | line-height: 1.4 256 | } 257 | 258 | .cover { 259 | vertical-align: middle; 260 | padding: 100px 261 | } 262 | 263 | footer { 264 | position: fixed; 265 | width: 100%; 266 | height: 40px; 267 | left: 0; 268 | bottom: 0; 269 | color: #a0a0a0; 270 | font-size: 14px 271 | } -------------------------------------------------------------------------------- /assets/template-zbr.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Error! 6 |

7 |
8 | 9 |
10 |

11 | 12 | {{ browzer_name }} Runtime code: 13 | 14 | 15 | {{ code }} 16 | 17 |

18 |
19 |
20 | {{ title }} 21 |
22 |
23 |
24 |
25 | {{ message }} 26 |
27 |
28 |
29 |
30 |
31 | Contact your Ziti Network Administrator for assistance. 32 |
33 |
34 |
35 | 40 | -------------------------------------------------------------------------------- /assets/template-zrok.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Oops! 6 |

7 | Ziggy 8 |
9 |

10 | 11 | the share you specified was not found 12 | 13 |

14 |
15 |
16 |

17 | 18 | The zrok share named 19 | 20 | 21 | {{ zrokshare }} 22 | 23 | 24 | might have been removed or is temporarily unavailable. 25 | 26 |

27 |
28 |
29 | 34 | -------------------------------------------------------------------------------- /assets/template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BrowZer Error Encountered 8 | 9 | 146 | 147 | 148 | <%- vars.body %> 149 | 150 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ZITI_BROWZER_BOOTSTRAPPER_TARGETS": "ZITI_BROWZER_BOOTSTRAPPER_TARGETS" 3 | } -------------------------------------------------------------------------------- /greenlock.d/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaults": { 3 | "agreeToTerms": true, 4 | "store": { 5 | "module": "greenlock-store-fs" 6 | }, 7 | "challenges": { 8 | "http-01": { 9 | "module": "acme-http-01-standalone" 10 | } 11 | }, 12 | "renewOffset": "-45d", 13 | "renewStagger": "3d", 14 | "accountKeyType": "EC-P256", 15 | "serverKeyType": "RSA-2048", 16 | "subscriberEmail": "openziti@netfoundry.io" 17 | }, 18 | "sites": [ 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /lib/edge/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | /** 92 | * The default timeout in milliseconds for connections and write operations to succeed. 93 | */ 94 | 'ZITI_DEFAULT_TIMEOUT': 10000, 95 | 96 | /** 97 | * Name of event indicating data send|recv to|from the wsER 98 | */ 99 | 'ZITI_EVENT_XGRESS': 'xgressEvent', 100 | 'ZITI_EVENT_XGRESS_TX': 'tx', 101 | 'ZITI_EVENT_XGRESS_RX': 'rx', 102 | 'ZITI_EVENT_INVALID_AUTH': 'invalidAuthEvent', 103 | 'ZITI_EVENT_NO_SERVICE': 'noServiceEvent', 104 | 'ZITI_EVENT_NO_CONFIG_FOR_SERVICE': 'noConfigForServiceEvent', 105 | 'ZITI_EVENT_SESSION_CREATION_ERROR': 'sessionCreationErrorEvent', 106 | 'ZITI_EVENT_NO_WSS_ROUTERS': 'noWSSEnabledEdgeRoutersEvent', 107 | 'ZITI_EVENT_IDP_AUTH_HEALTH': 'idpAuthHealthEvent', 108 | 'ZITI_EVENT_CHANNEL_CONNECT_FAIL': 'channelConnectFailEvent', 109 | 'ZITI_EVENT_NESTED_TLS_HANDSHAKE_TIMEOUT': 'nestedTLSHandshakeTimeout', 110 | 'ZITI_EVENT_NO_CONFIG_PROTOCOL_FOR_SERVICE': 'noConfigProtocolForServiceEvent', 111 | 112 | /** 113 | * Name of event indicating encrypted data for a nestedTLS connection has arrived and needs decryption 114 | */ 115 | 'ZITI_EVENT_XGRESS_RX_NESTED_TLS': 'xgressEventNestedTLS', 116 | 117 | }; 118 | 119 | module.exports = ZITI_CONSTANTS; 120 | -------------------------------------------------------------------------------- /lib/edge/context-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | /** 21 | * Default options. 22 | */ 23 | module.exports = defaultOptions = { 24 | 25 | /** 26 | * See {@link Options.logger} 27 | * 28 | */ 29 | logger: null, 30 | 31 | /** 32 | * See {@link Options.controllerApi} 33 | * 34 | */ 35 | controllerApi: 'https://local-controller:1280', 36 | 37 | /** 38 | * See {@link Options.token_type} 39 | * 40 | */ 41 | token_type: null, 42 | 43 | /** 44 | * See {@link Options.access_token} 45 | * 46 | */ 47 | access_token: null, 48 | 49 | /** 50 | * See {@link Options.sdkType} 51 | * 52 | */ 53 | sdkType: 'unknown', 54 | sdkVersion: 'unknown', 55 | sdkBranch: 'unknown', 56 | sdkRevision: 'unknown', 57 | 58 | /** 59 | * See {@link Options.apiSessionHeartbeatTime} 60 | * 61 | */ 62 | apiSessionHeartbeatTimeMin: (1), 63 | apiSessionHeartbeatTimeMax: (5), 64 | 65 | }; 66 | -------------------------------------------------------------------------------- /lib/edge/context.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | 22 | var flatOptions = require('./flat-options'); 23 | var defaultOptions = require('./context-options'); 24 | var ZITI_CONSTANTS = require('./constants'); 25 | var Mutex = require('async-mutex').Mutex; 26 | var withTimeout = require('async-mutex').withTimeout; 27 | const forEach = require('lodash.foreach'); 28 | const isUndefined = require('lodash.isundefined'); 29 | const isEqual = require('lodash.isequal'); 30 | const isNull = require('lodash.isnull'); 31 | const find = require('lodash.find'); 32 | const result = require('lodash.result'); 33 | var EventEmitter = require('events'); 34 | const uuidv4 = require('uuid').v4; 35 | 36 | 37 | /** 38 | * ZitiContext 39 | */ 40 | module.exports = class ZitiContext extends EventEmitter { 41 | 42 | /** 43 | * 44 | */ 45 | constructor(options) { 46 | 47 | super(); 48 | 49 | this._initialized = false; 50 | 51 | let _options = flatOptions(options, defaultOptions); 52 | 53 | this._keyType = _options.keyType; 54 | 55 | this.logger = _options.logger; 56 | this.controllerApi = _options.controllerApi; 57 | 58 | this.token_type = _options.token_type; 59 | this.access_token = _options.access_token; 60 | 61 | this.sdkType = _options.sdkType; 62 | this.sdkVersion = _options.sdkVersion; 63 | this.sdkBranch = _options.sdkBranch; 64 | this.sdkRevision = _options.sdkRevision; 65 | 66 | this.apiSessionHeartbeatTimeMin = _options.apiSessionHeartbeatTimeMin; 67 | this.apiSessionHeartbeatTimeMax = _options.apiSessionHeartbeatTimeMax; 68 | 69 | 70 | this._network_sessions = new Map(); 71 | this._services = []; 72 | 73 | 74 | this._ensureAPISessionMutex = withTimeout(new Mutex(), 3 * 1000); 75 | this._servicesMutex = withTimeout(new Mutex(), 3 * 1000, new Error('timeout on _servicesMutex')); 76 | 77 | this._apiSession = null; 78 | 79 | this._timeout = ZITI_CONSTANTS.ZITI_DEFAULT_TIMEOUT; 80 | 81 | this._uuid = uuidv4(); 82 | 83 | this.logger.trace({message: `ZitiContext uuid[${this._uuid}] instantiated `}); 84 | 85 | } 86 | 87 | updateAccessToken(access_token) { 88 | this._apiSession = null; 89 | this.access_token = access_token; 90 | } 91 | 92 | /** 93 | * 94 | */ 95 | async initialize(options) { 96 | 97 | if (this._initialized) throw Error("Already initialized; Cannot call .initialize() twice on instance."); 98 | 99 | this._zitiBrowzerEdgeClient = await this.createZitiBrowzerEdgeClient ({ 100 | logger: this.logger, 101 | controllerApi: this.controllerApi, 102 | domain: this.controllerApi, 103 | token_type: this.token_type, 104 | access_token: this.access_token, 105 | }); 106 | 107 | this._initialized = true; 108 | 109 | } 110 | 111 | /** 112 | * 113 | * @param {*} options 114 | * @returns ZitiContext 115 | */ 116 | async createZitiBrowzerEdgeClient (options) { 117 | 118 | const { ZitiBrowzerEdgeClient } = await import('@openziti/ziti-browzer-edge-client'); 119 | 120 | /** 121 | * The following will make the ZitiBrowzerEdgeClient believe it is running in a browser 122 | */ 123 | var { 124 | Headers, 125 | Request, 126 | Response, 127 | } = await import('node-fetch'); 128 | const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); 129 | if (!globalThis.fetch) { 130 | globalThis.fetch = fetch 131 | globalThis.Headers = Headers 132 | globalThis.Request = Request 133 | globalThis.Response = Response 134 | } 135 | 136 | let zitiBrowzerEdgeClient = new ZitiBrowzerEdgeClient(Object.assign( {}, options)) 137 | 138 | return zitiBrowzerEdgeClient; 139 | } 140 | 141 | 142 | /** 143 | * 144 | */ 145 | async doAuthenticate() { 146 | 147 | let self = this; 148 | 149 | return new Promise( async (resolve, reject) => { 150 | 151 | // Use 'ext-jwt' style authentication, but allow for 'password' style (mostly for testing) 152 | let method = (isNull(self.access_token)) ? 'password' : 'ext-jwt'; 153 | self.logger.trace(`ZitiContext.doAuthenticate(): method[${method}]`); 154 | 155 | // Get an API session with Controller 156 | let res = await self._zitiBrowzerEdgeClient.authenticate({ 157 | 158 | method: method, 159 | 160 | auth: { 161 | 162 | username: self.updbUser, 163 | password: self.updbPswd, 164 | 165 | configTypes: [ 166 | 'ziti-tunneler-client.v1', 167 | 'intercept.v1', 168 | 'zrok.proxy.v1' 169 | ], 170 | 171 | } 172 | }).catch((error) => { 173 | self.logger.error( error ); 174 | }); 175 | 176 | return resolve( res ); 177 | 178 | }); 179 | 180 | } 181 | 182 | delay(time) { 183 | return new Promise(resolve => setTimeout(resolve, time)); 184 | } 185 | 186 | /** 187 | * 188 | */ 189 | async getFreshAPISession() { 190 | 191 | this.logger.trace({message: `ZitiContext.getFreshAPISession() uuid[${this._uuid}] entered`}); 192 | 193 | let authenticated = false; 194 | let retry = 5; 195 | 196 | do { 197 | 198 | let res = await this.doAuthenticate(); 199 | 200 | if (isUndefined(res)) { 201 | 202 | this.logger.trace('ZitiContext.getFreshAPISession(): will retry after delay'); 203 | await this.delay(1000); 204 | retry--; 205 | 206 | } 207 | else if (!isUndefined(res.error)) { 208 | 209 | retry = 0; 210 | 211 | this.logger.error({message: `ZitiContext.getFreshAPISession(): Bootstrapper-to-Controller authentication failed`}); 212 | 213 | } else { 214 | 215 | this._apiSession = res.data; 216 | 217 | if (isUndefined( this._apiSession )) { 218 | 219 | this.logger.error('response contains no data'); 220 | this.logger.trace('ZitiContext.getFreshAPISession(): will retry after delay'); 221 | await this.delay(1000); 222 | retry--; 223 | 224 | } 225 | else if (isUndefined( this._apiSession.token )) { 226 | 227 | this.logger.error('response contains no token'); 228 | this.logger.trace('ZitiContext.getFreshAPISession(): will retry after delay'); 229 | await this.delay(1000); 230 | retry--; 231 | 232 | } 233 | else { 234 | 235 | // Set the token header on behalf of all subsequent Controller API calls 236 | this._zitiBrowzerEdgeClient.setApiKey(this._apiSession.token, 'zt-session', false); 237 | 238 | this.apiSessionHeartbeatId = setTimeout(this.apiSessionHeartbeat, this.getApiSessionHeartbeatTime(this), this ); 239 | 240 | authenticated = true; 241 | 242 | } 243 | 244 | } 245 | 246 | } while (!authenticated && retry > 0); 247 | 248 | if (!authenticated) { 249 | return null; 250 | } else { 251 | this.hasAPISession = true; 252 | } 253 | 254 | this.logger.trace({message: 'ZitiContext.getFreshAPISession() exiting', token: this._apiSession.token}); 255 | 256 | return this._apiSession.token ; 257 | } 258 | 259 | 260 | /** 261 | * 262 | */ 263 | async ensureAPISession() { 264 | 265 | let token; 266 | 267 | await this._ensureAPISessionMutex.runExclusive(async () => { 268 | if (isNull( this._apiSession ) || isUndefined( this._apiSession.token )) { 269 | token = await this.getFreshAPISession().catch((error) => { 270 | this.logger.error({message: `${error.message}`}); 271 | token = null; 272 | }); 273 | } else { 274 | token = this._apiSession.token; 275 | } 276 | }); 277 | 278 | if (isNull(token)) { 279 | throw new Error('Bootstrapper-to-Controller authentication failed') 280 | } 281 | 282 | return token; 283 | } 284 | 285 | 286 | /** 287 | * 288 | */ 289 | async apiSessionHeartbeat(self) { 290 | 291 | self.logger.trace({message: `ZitiContext.apiSessionHeartbeat() uuid[${self._uuid}] entered, deleted[${self.deleted}]`}); 292 | 293 | let res = await self._zitiBrowzerEdgeClient.getCurrentAPISession({ }).catch((error) => { 294 | throw error; 295 | }); 296 | 297 | let idpAuthHealthEvent = { 298 | zitiContext: self, // let consumers know who emitted the event 299 | expired: false // default is to assume JWT is NOT expired 300 | }; 301 | 302 | if (!isUndefined(res.error)) { 303 | 304 | self.logger.error(res.error.message); 305 | 306 | if (!isUndefined( self._apiSession )) { 307 | self._apiSession.token = null; 308 | } 309 | 310 | idpAuthHealthEvent.expired = true; 311 | 312 | } else { 313 | 314 | self._apiSession = res.data; 315 | if (isUndefined( self._apiSession )) { 316 | self.logger.warn('ZitiContext.apiSessionHeartbeat(): response contains no data:', res); 317 | idpAuthHealthEvent.expired = true; 318 | } 319 | 320 | if (isUndefined( self._apiSession.token )) { 321 | self.logger.warn('ZitiContext.apiSessionHeartbeat(): response contains no token:', res); 322 | idpAuthHealthEvent.expired = true; 323 | } 324 | 325 | if (Array.isArray( res.data.authQueries )) { 326 | forEach( res.data.authQueries, function( authQueryElement ) { 327 | if (isEqual(authQueryElement.typeId, 'EXT-JWT')) { 328 | idpAuthHealthEvent.expired = true; 329 | } 330 | }); 331 | } 332 | 333 | // Set the token header on behalf of all subsequent Controller API calls 334 | self._zitiBrowzerEdgeClient.setApiKey(self._apiSession.token, 'zt-session', false); 335 | 336 | self.logger.trace({message: `ZitiContext.apiSessionHeartbeat() uuid[${self._uuid}] exiting`, token: self._apiSession.token}); 337 | } 338 | 339 | // Let any listeners know the current IdP Auth health status 340 | self.emit(ZITI_CONSTANTS.ZITI_EVENT_IDP_AUTH_HEALTH, idpAuthHealthEvent); 341 | 342 | // Only recurse if this session hasn't expired 343 | if (!idpAuthHealthEvent.expired) { 344 | self.apiSessionHeartbeatId = setTimeout(self.apiSessionHeartbeat, self.getApiSessionHeartbeatTime(self), self ); 345 | } else { 346 | self.hasAPISession = false; 347 | } 348 | } 349 | 350 | 351 | awaitHasAPISession() { 352 | let ctx = this; 353 | return new Promise((resolve) => { 354 | (function waitForHasAPISession() { 355 | if (!ctx.hasAPISession) { 356 | setTimeout(waitForHasAPISession, 100); 357 | } else { 358 | return resolve(); 359 | } 360 | })(); 361 | }); 362 | } 363 | 364 | 365 | /** 366 | * 367 | */ 368 | getApiSessionHeartbeatTime(self) { 369 | let time = self.getRandomInt(self.apiSessionHeartbeatTimeMin, self.apiSessionHeartbeatTimeMax); 370 | self.logger.debug({message: `mins before next heartbeat: ${time}`}); 371 | return (time * 1000 * 60); 372 | } 373 | 374 | /** 375 | * Returns a random integer between min (inclusive) and max (inclusive). 376 | */ 377 | getRandomInt(min, max) { 378 | min = Math.ceil(min); 379 | max = Math.floor(max); 380 | return Math.floor(Math.random() * (max - min + 1)) + min; 381 | } 382 | 383 | 384 | /** 385 | * 386 | */ 387 | async fetchServices() { 388 | 389 | await this._servicesMutex.runExclusive(async () => { 390 | 391 | await this.ensureAPISession(); 392 | 393 | let done = false; 394 | let offset = 0; 395 | this._services = []; 396 | 397 | do { 398 | 399 | // Get list of active Services from Controller 400 | let res = await this._zitiBrowzerEdgeClient.listServices({ 401 | offset: offset, 402 | limit: '500' 403 | }).catch((error) => { 404 | throw error; 405 | }); 406 | 407 | if (!isUndefined(res.error)) { 408 | this.logger.error(res.error.message); 409 | throw new Error(res.error.message); 410 | } 411 | 412 | if (isEqual(res.data.length, 0)) { 413 | done = true; 414 | } else { 415 | for (let i=0; i < res.data.length; i++) { 416 | 417 | this._services.push(res.data[i]); 418 | offset++; 419 | 420 | if (offset >= res.meta.pagination.totalCount) { 421 | done = true; 422 | } 423 | } 424 | 425 | } 426 | 427 | } while (!done); 428 | 429 | }); 430 | 431 | } 432 | 433 | /** 434 | * 435 | */ 436 | async fetchServiceByName( name ) { 437 | 438 | let result = false; 439 | 440 | await this._servicesMutex.runExclusive(async () => { 441 | 442 | await this.ensureAPISession(); 443 | 444 | // Get the specified Service from Controller 445 | let res = await this._zitiBrowzerEdgeClient.listServices({ 446 | filter: `name="${name}"`, 447 | }).catch((error) => { 448 | return; 449 | }); 450 | 451 | if (!isUndefined(res.error)) { 452 | return; 453 | } 454 | 455 | for (let i=0; i < res.data.length; i++) { 456 | this._services.push(res.data[i]); 457 | } 458 | 459 | result = true; 460 | 461 | }); 462 | 463 | return result; 464 | 465 | } 466 | 467 | 468 | /** 469 | * 470 | */ 471 | async listControllerVersion() { 472 | 473 | let res = await this._zitiBrowzerEdgeClient.listVersion({ 474 | }).catch((error) => { 475 | throw error; 476 | }); 477 | 478 | if (!isUndefined(res.error)) { 479 | this.logger.error(res.error.message); 480 | throw new Error(res.error.message); 481 | } 482 | 483 | this._controllerVersion = res.data; 484 | 485 | if (isUndefined( this._controllerVersion ) ) { 486 | throw new Error('response contains no data'); 487 | } 488 | 489 | this.logger.info('Controller Version acquired: ', this._controllerVersion.version); 490 | 491 | return this._controllerVersion; 492 | } 493 | 494 | get controllerVersion () { 495 | return this._controllerVersion; 496 | } 497 | 498 | 499 | get services () { 500 | return this._services; 501 | } 502 | 503 | 504 | /** 505 | * 506 | */ 507 | async getServiceIdByName(name) { 508 | 509 | let self = this; 510 | let service_id; 511 | 512 | await this._servicesMutex.runExclusive(async () => { 513 | 514 | service_id = result(find(self._services, function(obj) { 515 | return obj.name === name; 516 | }), 'id'); 517 | 518 | }); 519 | 520 | return service_id; 521 | 522 | } 523 | 524 | } 525 | -------------------------------------------------------------------------------- /lib/edge/fetch-polyfill.js: -------------------------------------------------------------------------------- 1 | var fetch, { 2 | Blob, 3 | blobFrom, 4 | blobFromSync, 5 | File, 6 | fileFrom, 7 | fileFromSync, 8 | FormData, 9 | Headers, 10 | Request, 11 | Response, 12 | } = await import('node-fetch'); 13 | 14 | if (!globalThis.fetch) { 15 | globalThis.fetch = fetch 16 | globalThis.Headers = Headers 17 | globalThis.Request = Request 18 | globalThis.Response = Response 19 | } 20 | -------------------------------------------------------------------------------- /lib/edge/flat-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | module.exports = function flatOptions(options, defaults) { 18 | const result = Object.assign({}, defaults); 19 | if (options && typeof options === 'object') { 20 | Object.keys(options).forEach(key => validateOption(key, defaults) && copyOption(key, options, result)); 21 | } 22 | return result; 23 | } 24 | 25 | function copyOption(key, from, to) { 26 | if (from[key] !== undefined) { 27 | to[key] = from[key]; 28 | } 29 | } 30 | 31 | function validateOption(key, defaults) { 32 | if (defaults && !Object.hasOwnProperty.call(defaults, key)) { 33 | throw new Error(`Unknown option: ${key}`); 34 | } 35 | return true; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /lib/env.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 path = require('path'); 18 | var fs = require('fs'); 19 | var nconf = require('nconf'); 20 | const isEqual = require('lodash.isequal'); 21 | var nval = require('nconf-validator')(nconf); 22 | const Ajv = require("ajv"); 23 | const ajv = new Ajv({ validateFormats: true }); 24 | const addFormats = require("ajv-formats"); 25 | addFormats(ajv); // Adds support for formats like "uri", "email", "date", etc. 26 | 27 | /** 28 | * Reach into teh ZBR dependency, and obtain the "hashed" name of the ZBR CSS file. 29 | * This will be the default used 30 | */ 31 | function getZBRCSSname() { 32 | try { 33 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 34 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 35 | let zbrCSSName; 36 | fs.readdirSync(pathToZitiBrowzerRuntimeModule).forEach(file => { 37 | if (file.startsWith('ziti-browzer-css-')) { 38 | zbrCSSName = file; 39 | } 40 | }); 41 | return zbrCSSName; 42 | } 43 | catch (e) { 44 | console.error(e); 45 | } 46 | } 47 | 48 | /** ----------------------------------------------------------------------------------------------- 49 | * Config value Order-of-precedence is: 50 | * 1) cmd line args 51 | * 2) env vars 52 | * 3) config.json 53 | * 4) defaults 54 | * -----------------------------------------------------------------------------------------------*/ 55 | nconf 56 | .argv() 57 | .env() 58 | .file( 59 | { 60 | file: path.join(__dirname, '../config.json') 61 | } 62 | ) 63 | .defaults( 64 | { 65 | ZITI_BROWZER_BOOTSTRAPPER_LOGLEVEL: 'error', 66 | ZITI_BROWZER_BOOTSTRAPPER_SCHEME: 'http', 67 | ZITI_BROWZER_BOOTSTRAPPER_WILDCARD_VHOSTS: false, 68 | ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT: 80, 69 | 70 | ZITI_BROWZER_LOAD_BALANCER_PORT: 443, 71 | 72 | ZITI_CONTROLLER_PORT: 443, 73 | 74 | ZITI_BROWZER_RUNTIME_LOGLEVEL: 'error', 75 | ZITI_BROWZER_RUNTIME_HOTKEY: 'alt+f12', 76 | 77 | // This token is for *.cloudziti.io 78 | ZITI_BROWZER_RUNTIME_ORIGIN_TRIAL_TOKEN: `Arybv/l0GnolTvMW5WE4XZEnlFxRM7+WEgVLRXkGWj7/x6oyXrcXFXuSmhYOgIgr1SbF1McwU0ldUSQ6kGoMugMAAAB4eyJvcmlnaW4iOiJodHRwczovL2Nsb3Vkeml0aS5pbzo0NDMiLCJmZWF0dXJlIjoiV2ViQXNzZW1ibHlKU1Byb21pc2VJbnRlZ3JhdGlvbiIsImV4cGlyeSI6MTc1MzE0MjQwMCwiaXNTdWJkb21haW4iOnRydWV9`, 79 | 80 | NODE_EXTRA_CA_CERTS: 'node_modules/node_extra_ca_certs_mozilla_bundle/ca_bundle/ca_intermediate_root_bundle.pem', 81 | 82 | ZITI_BROWZER_WHITELABEL: JSON.stringify({ 83 | "enable": { 84 | "browZerButton": true, 85 | "browZerToastMessages": true, 86 | "browZerSplashScreen": true, 87 | "browZerThroughputChart": true 88 | }, 89 | "branding": { 90 | "browZerName": "OpenZiti BrowZer", 91 | "browZerSplashMessage": "Stand by while OpenZiti BrowZer Bootstraps your private web app", 92 | "browZerButtonIconSvgUrl": `https://${nconf.get('ZITI_BROWZER_BOOTSTRAPPER_HOST')}/ziti-browzer-logo.svg`, 93 | "browZerCSS": `https://${nconf.get('ZITI_BROWZER_BOOTSTRAPPER_HOST')}/${getZBRCSSname()}` 94 | } 95 | }), 96 | } 97 | ) 98 | .required( 99 | [ 100 | 'ZITI_BROWZER_BOOTSTRAPPER_TARGETS', 101 | 'ZITI_BROWZER_BOOTSTRAPPER_HOST', 102 | 'ZITI_CONTROLLER_HOST', 103 | ] 104 | ); 105 | 106 | const whitelabelSchema = { 107 | type: "object", 108 | properties: { 109 | enable: { 110 | type: "object", 111 | properties: { 112 | browZerButton: { type: "boolean" }, 113 | browZerToastMessages: { type: "boolean" }, 114 | browZerSplashScreen: { type: "boolean" }, 115 | browZerThroughputChart: { type: "boolean" } 116 | }, 117 | required: [ 118 | "browZerButton", 119 | "browZerToastMessages", 120 | "browZerSplashScreen", 121 | "browZerThroughputChart" 122 | ], 123 | additionalProperties: false // Prevent misspelled keys 124 | }, 125 | branding: { 126 | type: "object", 127 | properties: { 128 | browZerName: { type: "string" }, 129 | browZerSplashMessage: { type: "string" }, 130 | browZerButtonIconSvgUrl: { type: "string", format: "uri"}, 131 | browZerCSS: { type: "string", format: "uri"}, 132 | }, 133 | required: [ 134 | "browZerName", 135 | "browZerSplashMessage", 136 | "browZerButtonIconSvgUrl", 137 | "browZerCSS" 138 | ], 139 | additionalProperties: false // Prevent misspelled keys 140 | } 141 | }, 142 | additionalProperties: false // Prevent misspelled keys 143 | }; 144 | 145 | /** ----------------------------------------------------------------------------------------------- 146 | * config validation rules 147 | * -----------------------------------------------------------------------------------------------*/ 148 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_TARGETS', 'json'); 149 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_LOG_TAGS')) { 150 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_LOG_TAGS', 'json'); 151 | } 152 | 153 | nval.addRule('ZITI_BROWZER_WHITELABEL', 'json'); 154 | 155 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_WILDCARD_VHOSTS', Boolean); 156 | 157 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_SCHEME', ['http', 'https']); 158 | 159 | if (nconf.get('ZITI_BROWZER_LOAD_BALANCER_HOST')) { 160 | nval.addRule('ZITI_BROWZER_LOAD_BALANCER_HOST', 'domain') 161 | } 162 | 163 | // M2M OIDC-related config 164 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_IDP_BASE_URL')) { 165 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_IDP_BASE_URL', 'url') 166 | nconf.required([ 167 | 'ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_ID', 168 | 'ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_SECRET', 169 | 'ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_AUDIENCE' 170 | ]); 171 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_ID', String) 172 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_SECRET', String) 173 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_AUDIENCE', String) 174 | } 175 | 176 | if (nconf.get('ZITI_BROWZER_LOAD_BALANCER_PORT')) { 177 | nval.addRule('ZITI_BROWZER_LOAD_BALANCER_PORT', 'port') 178 | } 179 | 180 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_SKIP_CONTROLLER_CERT_CHECK')) { 181 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_SKIP_CONTROLLER_CERT_CHECK', Boolean) 182 | } 183 | 184 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_SKIP_DEPRECATION_WARNINGS')) { 185 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_SKIP_DEPRECATION_WARNINGS', Boolean) 186 | } 187 | 188 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_CERTIFICATE_PATH')) { 189 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_CERTIFICATE_PATH', String); 190 | } 191 | 192 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_KEY_PATH')) { 193 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_KEY_PATH', String); 194 | } 195 | 196 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_HOST', 'domain') 197 | 198 | nval.addRule('ZITI_CONTROLLER_HOST', 'domain') 199 | 200 | nval.addRule('ZITI_CONTROLLER_PORT', 'port') 201 | 202 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT')) { 203 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT', 'port'); 204 | } 205 | 206 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_LOGLEVEL', function(x) { 207 | if (! (typeof x === 'string')) { return false;} 208 | x = x.toLowerCase(); 209 | if (['error', 'warn', 'info', 'verbose', 'debug', 'silly'].includes(x)) { 210 | return true; 211 | } 212 | return false; 213 | }) 214 | 215 | if (nconf.get('ZITI_BROWZER_RUNTIME_ORIGIN_TRIAL_TOKEN')) { 216 | nval.addRule('ZITI_BROWZER_RUNTIME_ORIGIN_TRIAL_TOKEN', String); 217 | } 218 | 219 | if (nconf.get('ZITI_BROWZER_BOOTSTRAPPER_GITHUB_API_TOKEN')) { 220 | nval.addRule('ZITI_BROWZER_BOOTSTRAPPER_GITHUB_API_TOKEN', String); 221 | } 222 | 223 | 224 | /** 225 | * Now validate the config 226 | */ 227 | nval.validate(); 228 | 229 | const validateWhitelabel = ajv.compile(whitelabelSchema); 230 | const whitelabelConfigData = JSON.parse(nconf.get('ZITI_BROWZER_WHITELABEL')); 231 | const valid = validateWhitelabel(whitelabelConfigData); 232 | 233 | if (!valid) { 234 | console.error("Whitelabel Configuration validation failed:", validateWhitelabel.errors); 235 | throw new Error("Whitelabel Configuration validation failed") 236 | } 237 | 238 | function asBool (value) { 239 | const val = value.toLowerCase() 240 | 241 | const allowedValues = [ 242 | 'false', 243 | '0', 244 | 'true', 245 | '1' 246 | ] 247 | 248 | if (allowedValues.indexOf(val) === -1) { 249 | throw new Error('should be either "true", "false", "TRUE", "FALSE", 1, or 0') 250 | } 251 | 252 | return !(((val === '0') || (val === 'false'))) 253 | } 254 | 255 | module.exports = function(key) { 256 | let val = nconf.get(key); 257 | if (val && (isEqual(key, 'ZITI_BROWZER_BOOTSTRAPPER_WILDCARD_VHOSTS') 258 | || isEqual(key, 'ZITI_BROWZER_BOOTSTRAPPER_SKIP_CONTROLLER_CERT_CHECK') 259 | || isEqual(key, 'ZITI_BROWZER_BOOTSTRAPPER_SKIP_DEPRECATION_WARNINGS')) 260 | ) { 261 | val = asBool(val); 262 | } 263 | return val; 264 | }; -------------------------------------------------------------------------------- /lib/http-proxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 explicit /index.js to help browserify negotiation in require '/lib/http-proxy' (!) 18 | var ProxyServer = require('./http-proxy/index.js').Server; 19 | 20 | 21 | /** 22 | * Creates the proxy server. 23 | * 24 | * Examples: 25 | * 26 | * httpProxy.createProxyServer({ .. }, 8000) 27 | * // => '{ web: [Function], ... }' 28 | * 29 | * @param {Object} Options Config object passed to the proxy 30 | * 31 | * @return {Object} Proxy Proxy object with handlers for `web` requests 32 | * 33 | * @api public 34 | */ 35 | 36 | 37 | function createProxyServer(options) { 38 | /* 39 | * `options` is needed and it must have the following layout: 40 | * 41 | * { 42 | * target : 43 | * forward: 44 | * agent : 45 | * ssl : 46 | * xfwd : 47 | * secure : 48 | * toProxy: 49 | * prependPath: 50 | * ignorePath: 51 | * localAddress : 52 | * changeOrigin: 53 | * preserveHeaderKeyCase: 54 | * auth : Basic authentication i.e. 'user:password' to compute an Authorization header. 55 | * hostRewrite: rewrites the location hostname on (201/301/302/307/308) redirects, Default: null. 56 | * autoRewrite: rewrites the location host/port on (201/301/302/307/308) redirects based on requested host/port. Default: false. 57 | * protocolRewrite: rewrites the location protocol on (201/301/302/307/308) redirects to 'http' or 'https'. Default: null. 58 | * } 59 | * 60 | * NOTE: `options.ssl` is optional. 61 | * `options.target and `options.forward` cannot be 62 | * both missing 63 | * } 64 | */ 65 | 66 | return new ProxyServer(options); 67 | } 68 | 69 | 70 | ProxyServer.createProxyServer = createProxyServer; 71 | ProxyServer.createServer = createProxyServer; 72 | ProxyServer.createProxy = createProxyServer; 73 | 74 | 75 | 76 | 77 | /** 78 | * Export the proxy "Server" as the main export. 79 | */ 80 | module.exports = ProxyServer; 81 | 82 | -------------------------------------------------------------------------------- /lib/http-proxy/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | var common = exports, 19 | env = require('../env'), 20 | url = require('url'), 21 | required = require('requires-port'), 22 | fs = require('fs'), 23 | requestIp = require('request-ip'), 24 | find = require('lodash.find'), 25 | { isEqual, isUndefined } = require('lodash'), 26 | swpjson = require('@openziti/ziti-browzer-sw/package.json'), 27 | getAccessToken = require('../../lib/oidc/utils').getAccessToken, 28 | ZITI_CONSTANTS = require('../../lib/edge/constants'), 29 | ZitiContext = require('../../lib/edge/context'), 30 | Mutex = require('async-mutex').Mutex, 31 | withTimeout = require('async-mutex').withTimeout, 32 | pjson = require('../../package.json'); 33 | 34 | var zitiContextMutex = withTimeout(new Mutex(), 3 * 1000, new Error('timeout on zitiContextMutex')); 35 | 36 | 37 | var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i, 38 | isSSL = /^https/; 39 | 40 | /** 41 | * Simple Regex for testing if protocol is https 42 | */ 43 | common.isSSL = isSSL; 44 | 45 | common.getConfigValue = function( ...args ) { 46 | return env( args ); 47 | } 48 | 49 | /** 50 | * 51 | */ 52 | var logLevelMap = new Map(); 53 | var logLevel = env('ZITI_BROWZER_RUNTIME_LOGLEVEL'); 54 | logLevelMap.set('*', logLevel); 55 | 56 | function mapEntriesToString(entries) { 57 | return Array 58 | .from(entries, ([k, v]) => `${k}:${v}, `) 59 | .join("") + ""; 60 | } 61 | 62 | common.logLevelSet = function (key, val) { 63 | logLevelMap.set(key, val); 64 | }; 65 | 66 | common.logLevelGet = function () { 67 | return mapEntriesToString(logLevelMap.entries()); 68 | }; 69 | 70 | common.logLevelGetForClient = function (client) { 71 | let level = logLevelMap.get(client); 72 | if (typeof level === 'undefined') { 73 | level = logLevel ? logLevel : 'error'; 74 | } 75 | return level; 76 | }; 77 | 78 | common.removeValue = function(list, value, separator) { 79 | if (typeof list === 'undefined') return list; 80 | separator = separator || ","; 81 | var values = list.split(separator); 82 | for(var i = 0 ; i < values.length ; i++) { 83 | if(values[i].trim() == value) { 84 | values.splice(i, 1); 85 | return values.join(separator); 86 | } 87 | } 88 | return list; 89 | } 90 | 91 | /** 92 | * Copies the right headers from `options` and `req` to 93 | * `outgoing` which is then used to fire the proxied 94 | * request. 95 | * 96 | * Examples: 97 | * 98 | * common.setupOutgoing(outgoing, options, req) 99 | * // => { host: ..., hostname: ...} 100 | * 101 | * @param {Object} Outgoing Base object to be filled with required properties 102 | * @param {Object} Options Config object passed to the proxy 103 | * @param {ClientRequest} Req Request Object 104 | * @param {String} Forward String to select forward or target 105 | *  106 | * @return {Object} Outgoing Object with all required properties set 107 | * 108 | * @api private 109 | */ 110 | 111 | common.setupOutgoing = function(outgoing, options, req, forward) { 112 | 113 | outgoing.port = options[forward || 'target'].port || 114 | (isSSL.test(options[forward || 'target'].protocol) ? 443 : 80); 115 | 116 | [ 117 | 'host', 118 | 'hostname', 119 | 'socketPath', 120 | 'pfx', 121 | 'key', 122 | 'passphrase', 123 | 'cert', 124 | 'ca', 125 | 'ciphers', 126 | 'secureProtocol', 127 | 'protocol' 128 | ].forEach( 129 | function(e) { outgoing[e] = options[forward || 'target'][e]; } 130 | ); 131 | 132 | outgoing.method = options.method || req.method; 133 | outgoing.headers = Object.assign({}, req.headers); 134 | 135 | if (options.headers){ 136 | Object.assign(outgoing.headers, options.headers); 137 | } 138 | 139 | // Prevent this from being sent (results in 500 errors on initial TSPlus requests) 140 | delete outgoing.headers['if-modified-since']; 141 | 142 | // Prevent 'br' from being sent as a viable 'Accept-Encoding' (we do not support the Brotli algorithm here) 143 | let val = common.removeValue(outgoing.headers['accept-encoding'], 'br'); 144 | if (val === "") { 145 | delete outgoing.headers['accept-encoding']; 146 | } else { 147 | outgoing.headers['accept-encoding'] = val; 148 | } 149 | 150 | if (options.auth) { 151 | outgoing.auth = options.auth; 152 | } 153 | 154 | if (options.ca) { 155 | outgoing.ca = options.ca; 156 | } 157 | 158 | if (isSSL.test(options[forward || 'target'].protocol)) { 159 | outgoing.rejectUnauthorized = (typeof options.secure === "undefined") ? true : options.secure; 160 | } 161 | 162 | outgoing.agent = options.agent || false; 163 | outgoing.localAddress = options.localAddress; 164 | 165 | // 166 | // Remark: If we are false and not upgrading, set the connection: close. This is the right thing to do 167 | // as node core doesn't handle this COMPLETELY properly yet. 168 | // 169 | if (!outgoing.agent) { 170 | outgoing.headers = outgoing.headers || {}; 171 | if (typeof outgoing.headers.connection !== 'string' 172 | || !upgradeHeader.test(outgoing.headers.connection) 173 | ) { outgoing.headers.connection = 'close'; } 174 | } 175 | 176 | // the final path is target path + relative path requested by user: 177 | var target = options[forward || 'target']; 178 | var targetPath = target && options.prependPath !== false 179 | ? (target.path || '') 180 | : ''; 181 | 182 | var outgoingPath = !options.toProxy 183 | ? (url.parse(req.url).path || '') 184 | : req.url; 185 | 186 | // 187 | // Remark: ignorePath will just straight up ignore whatever the request's 188 | // path is. This can be labeled as FOOT-GUN material if you do not know what 189 | // you are doing and are using conflicting options. 190 | // 191 | outgoingPath = !options.ignorePath ? outgoingPath : ''; 192 | 193 | outgoing.path = common.urlJoin(targetPath, outgoingPath); 194 | 195 | if (options.changeOrigin) { 196 | outgoing.headers.host = 197 | required(outgoing.port, options[forward || 'target'].protocol) && !hasPort(outgoing.host) 198 | ? outgoing.host + ':' + outgoing.port 199 | : outgoing.host; 200 | } 201 | 202 | return outgoing; 203 | 204 | }; 205 | 206 | 207 | /** 208 | * Generates the config for the Ziti browZer Runtime 209 | * 210 | *  211 | * @return {Object} config Object 212 | * 213 | * @api private 214 | */ 215 | 216 | common.generateZitiConfig = function(url, client) { 217 | 218 | var zc = common.generateZitiConfigObject(url, client); 219 | 220 | var ziti_config = `var zitiConfig = ` + JSON.stringify(zc); 221 | 222 | return ziti_config; 223 | } 224 | 225 | common.getZBRname = function() { 226 | 227 | try { 228 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 229 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 230 | let zbrName; 231 | fs.readdirSync(pathToZitiBrowzerRuntimeModule).forEach(file => { 232 | if (file.startsWith('ziti-browzer-runtime')) { 233 | zbrName = file; 234 | } 235 | }); 236 | 237 | return zbrName; 238 | 239 | } 240 | catch (e) { 241 | console.error(e); 242 | } 243 | 244 | } 245 | 246 | common.getZBRCSSname = function() { 247 | 248 | try { 249 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 250 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 251 | let zbrCSSName; 252 | fs.readdirSync(pathToZitiBrowzerRuntimeModule).forEach(file => { 253 | if (file.startsWith('ziti-browzer-css-')) { 254 | zbrCSSName = file; 255 | } 256 | }); 257 | 258 | return zbrCSSName; 259 | 260 | } 261 | catch (e) { 262 | console.error(e); 263 | } 264 | 265 | } 266 | 267 | common.generateZitiConfigObject = function(url, req, options) { 268 | 269 | var client = requestIp.getClientIp(req); 270 | var zitiClient = client || '*'; 271 | 272 | var browzer_bootstrapper_host = req.get('host'); 273 | 274 | var u = new URL(`https://${browzer_bootstrapper_host}`); 275 | 276 | var target; 277 | 278 | if (env('ZITI_BROWZER_BOOTSTRAPPER_WILDCARD_VHOSTS')) { 279 | 280 | target = find(options.targetArray, { 281 | vhost: `*` 282 | }); 283 | 284 | target_service = req.ziti_vhost; 285 | 286 | target_path = target.path; 287 | target_scheme = target.scheme; 288 | 289 | } else { 290 | 291 | target = find(options.targetArray, { 292 | vhost: u.hostname 293 | }); 294 | 295 | if (typeof target === 'undefined') { 296 | options.logger.error({message: 'Host header has no match in targets array', host: browzer_bootstrapper_host}); 297 | target_service = 'UNKNOWN'; 298 | target_path = '/'; 299 | target_scheme = 'https'; 300 | } else { 301 | target_service = target.service; 302 | target_path = target.path; 303 | target_scheme = target.scheme; 304 | } 305 | } 306 | 307 | var ziti_controller_host = env('ZITI_CONTROLLER_HOST'); 308 | var ziti_controller_port = env('ZITI_CONTROLLER_PORT'); 309 | var browzer_bootstrapper_scheme = env('ZITI_BROWZER_BOOTSTRAPPER_SCHEME'); 310 | var browzer_bootstrapper_listen_port = env('ZITI_BROWZER_BOOTSTRAPPER_LISTEN_PORT'); 311 | var idp_issuer_url = req.ziti_idp_issuer_base_url; 312 | var browzer_load_balancer = env('ZITI_BROWZER_LOAD_BALANCER_HOST'); 313 | var browzer_load_balancer_port = env('ZITI_BROWZER_LOAD_BALANCER_PORT'); 314 | if (!browzer_load_balancer_port) { 315 | browzer_load_balancer_port = 443; 316 | } 317 | 318 | let selfHost; 319 | if (env('ZITI_BROWZER_BOOTSTRAPPER_WILDCARD_VHOSTS')) { 320 | selfHost = `${req.ziti_vhost}.${ common.trimFirstSection( env('ZITI_BROWZER_BOOTSTRAPPER_HOST') )}` 321 | } else { 322 | selfHost = `${target.vhost}`; 323 | } 324 | 325 | let whitelabel = JSON.parse( env('ZITI_BROWZER_WHITELABEL')); 326 | let svgurl = new URL(whitelabel.branding.browZerButtonIconSvgUrl); 327 | if (isEqual(browzer_load_balancer, svgurl.hostname)) { 328 | svgurl.hostname = selfHost; 329 | whitelabel.branding.browZerButtonIconSvgUrl = svgurl.toString(); 330 | } 331 | let cssurl = new URL(whitelabel.branding.browZerCSS); 332 | if (isEqual(browzer_load_balancer, cssurl.hostname)) { 333 | cssurl.hostname = selfHost; 334 | whitelabel.branding.browZerCSS = cssurl.toString(); 335 | } 336 | 337 | var ziti_config = 338 | { 339 | controller: { 340 | api: `https://${ziti_controller_host}:${ziti_controller_port}/edge/client/v1` 341 | }, 342 | browzer: { 343 | bootstrapper: { 344 | self: { 345 | scheme: `${browzer_bootstrapper_scheme}`, 346 | host: `${selfHost}`, 347 | port: `${browzer_bootstrapper_listen_port}`, 348 | version: `${pjson.version}`, 349 | latestReleaseVersion: options.getLatestBrowZerReleaseVersion(), 350 | }, 351 | target: { 352 | service: `${target_service}`, 353 | path: `${target_path}`, 354 | scheme: `${target_scheme}` 355 | }, 356 | }, 357 | sw: { 358 | location: `ziti-browzer-sw.js`, 359 | version: `${swpjson.version}`, 360 | logLevel: `${common.logLevelGetForClient(zitiClient)}`, 361 | }, 362 | runtime: { 363 | src: `${common.getZBRname()}`, 364 | css: `${common.getZBRCSSname()}`, 365 | logLevel: `${common.logLevelGetForClient(zitiClient)}`, 366 | originTrialToken: `${common.getOriginTrialToken()}`, 367 | skipDeprecationWarnings: env('ZITI_BROWZER_BOOTSTRAPPER_SKIP_DEPRECATION_WARNINGS'), 368 | }, 369 | loadbalancer: { 370 | host: browzer_load_balancer ? `${browzer_load_balancer}` : undefined, 371 | port: browzer_load_balancer ? `${browzer_load_balancer_port}` : undefined 372 | }, 373 | whitelabel: whitelabel, 374 | }, 375 | idp: { 376 | host: `${idp_issuer_url}`, 377 | clientId: `${req.ziti_idp_client_id}`, 378 | authorization_endpoint_parms: req.ziti_idp_authorization_endpoint_parms ? `${req.ziti_idp_authorization_endpoint_parms}` : undefined, 379 | authorization_scope: req.ziti_idp_authorization_scope ? `${req.ziti_idp_authorization_scope}` : undefined, 380 | } 381 | }; 382 | 383 | 384 | return ziti_config; 385 | } 386 | 387 | /** 388 | * Set the proper configuration for sockets, 389 | * set no delay and set keep alive, also set 390 | * the timeout to 0. 391 | * 392 | * Examples: 393 | * 394 | * common.setupSocket(socket) 395 | * // => Socket 396 | * 397 | * @param {Socket} Socket instance to setup 398 | *  399 | * @return {Socket} Return the configured socket. 400 | * 401 | * @api private 402 | */ 403 | 404 | common.setupSocket = function(socket) { 405 | socket.setTimeout(0); 406 | socket.setNoDelay(true); 407 | 408 | socket.setKeepAlive(true, 0); 409 | 410 | return socket; 411 | }; 412 | 413 | /** 414 | * Get the port number from the host. Or guess it based on the connection type. 415 | * 416 | * @param {Request} req Incoming HTTP request. 417 | * 418 | * @return {String} The port number. 419 | * 420 | * @api private 421 | */ 422 | common.getPort = function(req) { 423 | var res = req.headers.host ? req.headers.host.match(/:(\d+)/) : ''; 424 | 425 | return res ? 426 | res[1] : 427 | common.hasEncryptedConnection(req) ? '443' : '80'; 428 | }; 429 | 430 | /** 431 | * Check if the request has an encrypted connection. 432 | * 433 | * @param {Request} req Incoming HTTP request. 434 | * 435 | * @return {Boolean} Whether the connection is encrypted or not. 436 | * 437 | * @api private 438 | */ 439 | common.hasEncryptedConnection = function(req) { 440 | return Boolean(req.connection.encrypted || req.connection.pair); 441 | }; 442 | 443 | /** 444 | * OS-agnostic join (doesn't break on URLs like path.join does on Windows)> 445 | * 446 | * @return {String} The generated path. 447 | * 448 | * @api private 449 | */ 450 | 451 | common.urlJoin = function() { 452 | // 453 | // We do not want to mess with the query string. All we want to touch is the path. 454 | // 455 | var args = Array.prototype.slice.call(arguments), 456 | lastIndex = args.length - 1, 457 | last = args[lastIndex], 458 | lastSegs = last.split('?'), 459 | retSegs; 460 | 461 | args[lastIndex] = lastSegs.shift(); 462 | 463 | // 464 | // Join all strings, but remove empty strings so we don't get extra slashes from 465 | // joining e.g. ['', 'am'] 466 | // 467 | retSegs = [ 468 | args.filter(Boolean).join('/') 469 | .replace(/\/+/g, '/') 470 | .replace('http:/', 'http://') 471 | .replace('https:/', 'https://') 472 | ]; 473 | 474 | // Only join the query string if it exists so we don't have trailing a '?' 475 | // on every request 476 | 477 | // Handle case where there could be multiple ? in the URL. 478 | retSegs.push.apply(retSegs, lastSegs); 479 | 480 | return retSegs.join('?') 481 | }; 482 | 483 | /** 484 | * Rewrites or removes the domain of a cookie header 485 | * 486 | * @param {String|Array} Header 487 | * @param {Object} Config, mapping of domain to rewritten domain. 488 | * '*' key to match any domain, null value to remove the domain. 489 | * 490 | * @api private 491 | */ 492 | common.rewriteCookieProperty = function rewriteCookieProperty(header, config, property) { 493 | if (Array.isArray(header)) { 494 | return header.map(function (headerElement) { 495 | return rewriteCookieProperty(headerElement, config, property); 496 | }); 497 | } 498 | return header.replace(new RegExp("(;\\s*" + property + "=)([^;]+)", 'i'), function(match, prefix, previousValue) { 499 | var newValue; 500 | if (previousValue in config) { 501 | newValue = config[previousValue]; 502 | } else if ('*' in config) { 503 | newValue = config['*']; 504 | } else { 505 | //no match, return previous value 506 | return match; 507 | } 508 | if (newValue) { 509 | //replace value 510 | return prefix + newValue; 511 | } else { 512 | //remove value 513 | return ''; 514 | } 515 | }); 516 | }; 517 | 518 | /** 519 | * Check the host and see if it potentially has a port in it (keep it simple) 520 | * 521 | * @returns {Boolean} Whether we have one or not 522 | * 523 | * @api private 524 | */ 525 | function hasPort(host) { 526 | return !!~host.indexOf(':'); 527 | }; 528 | 529 | 530 | /** 531 | * Determine of a value is a boolean or not 532 | */ 533 | common.toBool = function (item) { 534 | switch(typeof item) { 535 | case "boolean": 536 | return item; 537 | case "function": 538 | return true; 539 | case "number": 540 | return item > 0 || item < 0; 541 | case "object": 542 | return !!item; 543 | case "string": 544 | item = item.toLowerCase(); 545 | return ["true", "1"].indexOf(item) >= 0; 546 | case "symbol": 547 | return true; 548 | case "undefined": 549 | return false; 550 | 551 | default: 552 | throw new TypeError("Unrecognised type: unable to convert to boolean"); 553 | } 554 | }; 555 | 556 | 557 | /** 558 | * Generates the config for the Ziti browZer Runtime 559 | * 560 | */ 561 | common.generateAccessControlAllowOrigin = function(req) { 562 | 563 | return `https://${req.ziti_vhost}`; 564 | 565 | } 566 | 567 | 568 | /** 569 | * Determine if 'path' is one on the 'target path' 570 | */ 571 | common.isRequestOnTargetPath = function( req, options, path ) { 572 | 573 | let result = false; 574 | 575 | if (req.ziti_target_path) { 576 | let pathNoQuery = path.replace(/\?.*$/,""); 577 | let regex = new RegExp( pathNoQuery + '$', 'g' ); 578 | let hit = (req.ziti_target_path.match(regex) || []).length; 579 | if ((hit > 0)) { 580 | options.logger.silly({message: 'common.isRequestOnTargetPath: HIT on path', path: path, clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 581 | result = true; 582 | } 583 | } 584 | 585 | return result; 586 | } 587 | 588 | common.addServerHeader = function(headerObj) { 589 | return Object.assign(headerObj, { 590 | 'Server': `ziti-browzer-bootstrapper/${pjson.version}` 591 | }); 592 | } 593 | 594 | common.getOriginTrialToken = function() { 595 | var browzer_origin_trial_token = env('ZITI_BROWZER_RUNTIME_ORIGIN_TRIAL_TOKEN') 596 | return browzer_origin_trial_token; 597 | } 598 | 599 | 600 | /** -------------------------------------------------------------------------------------------------- 601 | * Spin up a fresh zitiContext 602 | */ 603 | var zitiContext; 604 | common.newZitiContext = async ( logger ) => { 605 | 606 | await zitiContextMutex.runExclusive(async () => { 607 | 608 | // If we have an active context, release any associated resources 609 | if (!isUndefined(zitiContext)) { 610 | logger.silly({message: `now destroying zitiContext[${zitiContext._uuid}] - apiSessionHeartbeat has been cleared`}); 611 | clearTimeout(zitiContext.apiSessionHeartbeatId); 612 | zitiContext.deleted = true; 613 | zitiContext = undefined; 614 | } 615 | 616 | // Instantiate/initialize the zitiContext we will use to obtain Service info from the Controller 617 | zitiContext = new ZitiContext(Object.assign({ 618 | logger: logger, 619 | controllerApi: `https://${env('ZITI_CONTROLLER_HOST')}:${env('ZITI_CONTROLLER_PORT')}/edge/client/v1`, 620 | token_type: `Bearer`, 621 | access_token: await getAccessToken( logger ), 622 | })); 623 | await zitiContext.initialize( {} ); 624 | 625 | // Monitor M2M JWT expiration events 626 | zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_IDP_AUTH_HEALTH, common.idpAuthHealthEventHandler); 627 | 628 | await zitiContext.ensureAPISession(); 629 | 630 | }); 631 | 632 | }; 633 | 634 | /** -------------------------------------------------------------------------------------------------- 635 | * Refresh the M2M IdP access token if it has expired 636 | */ 637 | common.idpAuthHealthEventHandler = async ( idpAuthHealthEvent ) => { 638 | 639 | if (!idpAuthHealthEvent.zitiContext.deleted) { 640 | if (idpAuthHealthEvent.expired) { 641 | common.newZitiContext( idpAuthHealthEvent.zitiContext.logger ); 642 | } 643 | } 644 | 645 | }; 646 | 647 | 648 | common.setZitiContext = function(context) { 649 | zitiContext = context; 650 | } 651 | common.getZitiContext = async function() { 652 | let ctx; 653 | await zitiContextMutex.runExclusive( () => { 654 | ctx = zitiContext; 655 | }); 656 | return ctx; 657 | } 658 | 659 | common.trimFirstSection = function(hostname) { 660 | const sections = hostname.split('.'); 661 | sections.shift(); 662 | const trimmedHostname = sections.join('.'); 663 | return trimmedHostname; 664 | } 665 | 666 | common.delay = function(time) { 667 | return new Promise(resolve => setTimeout(resolve, time)); 668 | } 669 | -------------------------------------------------------------------------------- /lib/http-proxy/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | var httpProxy = module.exports, 19 | extend = require('util')._extend, 20 | parse_url = require('url').parse, 21 | EE3 = require('eventemitter3'), 22 | http = require('http'), 23 | https = require('https'), 24 | web = require('./passes/web-incoming-zitified'); 25 | 26 | httpProxy.Server = ProxyServer; 27 | 28 | 29 | /** 30 | * Returns a function that creates the loader for 31 | * `web`'s passes. 32 | * 33 | * Examples: 34 | * 35 | * httpProxy.createRightProxy('web') 36 | * // => [Function] 37 | * 38 | * @param {String} Type Must be 'web' 39 | *  40 | * @return {Function} Loader Function that when called returns an iterator for the right passes 41 | * 42 | * @api private 43 | */ 44 | 45 | function createRightProxy(type) { 46 | 47 | return function(options) { 48 | return function(req, res /*, [head], [opts] */) { 49 | var passes = this.webPasses, 50 | args = [].slice.call(arguments), 51 | cntr = args.length - 1, 52 | head, cbl; 53 | 54 | /* optional args parse begin */ 55 | if(typeof args[cntr] === 'function') { 56 | cbl = args[cntr]; 57 | 58 | cntr--; 59 | } 60 | 61 | var requestOptions = options; 62 | if( 63 | !(args[cntr] instanceof Buffer) && 64 | args[cntr] !== res 65 | ) { 66 | //Copy global options 67 | requestOptions = extend({}, options); 68 | //Overwrite with request options 69 | extend(requestOptions, args[cntr]); 70 | 71 | cntr--; 72 | } 73 | 74 | if(args[cntr] instanceof Buffer) { 75 | head = args[cntr]; 76 | } 77 | 78 | /* optional args parse end */ 79 | 80 | [ 'target' ].forEach(function(e) { 81 | if (typeof requestOptions[e] === 'string') 82 | requestOptions[e] = parse_url(requestOptions[e]); 83 | }); 84 | 85 | for(var i=0; i < passes.length; i++) { 86 | /** 87 | * Call of passes functions 88 | * pass(req, res, options, head) 89 | */ 90 | if(passes[i](req, res, requestOptions, head, this, cbl)) { // passes can return a truthy value to halt the loop 91 | break; 92 | } 93 | } 94 | }; 95 | }; 96 | } 97 | httpProxy.createRightProxy = createRightProxy; 98 | 99 | function ProxyServer(options) { 100 | EE3.call(this); 101 | 102 | options = options || {}; 103 | options.prependPath = options.prependPath === false ? false : true; 104 | 105 | this.web = this.proxyRequest = createRightProxy('web')(options); 106 | this.options = options; 107 | 108 | this.webPasses = Object.keys(web).map(function(pass) { 109 | return web[pass]; 110 | }); 111 | 112 | this.on('error', this.onError, this); 113 | 114 | } 115 | 116 | require('util').inherits(ProxyServer, EE3); 117 | 118 | ProxyServer.prototype.onError = function (err) { 119 | // 120 | // Remark: Replicate node core behavior using EE3 121 | // so we force people to handle their own errors 122 | // 123 | if(this.listeners('error').length === 1) { 124 | throw err; 125 | } 126 | }; 127 | 128 | ProxyServer.prototype.listen = function(port, hostname) { 129 | var self = this, 130 | closure = function(req, res) { self.web(req, res); }; 131 | 132 | this._server = this.options.ssl ? 133 | https.createServer(this.options.ssl, closure) : 134 | http.createServer(closure); 135 | 136 | this._server.listen(port, hostname); 137 | 138 | return this; 139 | }; 140 | 141 | ProxyServer.prototype.close = function(callback) { 142 | var self = this; 143 | if (this._server) { 144 | this._server.close(done); 145 | } 146 | 147 | // Wrap callback to nullify server after all open connections are closed. 148 | function done() { 149 | self._server = null; 150 | if (callback) { 151 | callback.apply(null, arguments); 152 | } 153 | }; 154 | }; 155 | 156 | ProxyServer.prototype.before = function(type, passName, callback) { 157 | if (type !== 'web') { 158 | throw new Error('type must be `web`'); 159 | } 160 | var passes = this.webPasses, 161 | i = false; 162 | 163 | passes.forEach(function(v, idx) { 164 | if(v.name === passName) i = idx; 165 | }) 166 | 167 | if(i === false) throw new Error('No such pass'); 168 | 169 | passes.splice(i, 0, callback); 170 | }; 171 | ProxyServer.prototype.after = function(type, passName, callback) { 172 | if (type !== 'web') { 173 | throw new Error('type must be `web`'); 174 | } 175 | var passes = this.webPasses, 176 | i = false; 177 | 178 | passes.forEach(function(v, idx) { 179 | if(v.name === passName) i = idx; 180 | }) 181 | 182 | if(i === false) throw new Error('No such pass'); 183 | 184 | passes.splice(i++, 0, callback); 185 | }; 186 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/cors-proxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 corsProxy = exports, 18 | url = require('url'), 19 | net = require('net'), 20 | common = require('../common'), 21 | httpProxy = require('http-proxy'); 22 | 23 | var regexp_tld = /\.(?:AAA|AARP|ABARTH|ABB|ABBOTT|ABBVIE|ABC|ABLE|ABOGADO|ABUDHABI|AC|ACADEMY|ACCENTURE|ACCOUNTANT|ACCOUNTANTS|ACO|ACTOR|AD|ADAC|ADS|ADULT|AE|AEG|AERO|AETNA|AF|AFAMILYCOMPANY|AFL|AFRICA|AG|AGAKHAN|AGENCY|AI|AIG|AIRBUS|AIRFORCE|AIRTEL|AKDN|AL|ALFAROMEO|ALIBABA|ALIPAY|ALLFINANZ|ALLSTATE|ALLY|ALSACE|ALSTOM|AM|AMAZON|AMERICANEXPRESS|AMERICANFAMILY|AMEX|AMFAM|AMICA|AMSTERDAM|ANALYTICS|ANDROID|ANQUAN|ANZ|AO|AOL|APARTMENTS|APP|APPLE|AQ|AQUARELLE|AR|ARAB|ARAMCO|ARCHI|ARMY|ARPA|ART|ARTE|AS|ASDA|ASIA|ASSOCIATES|AT|ATHLETA|ATTORNEY|AU|AUCTION|AUDI|AUDIBLE|AUDIO|AUSPOST|AUTHOR|AUTO|AUTOS|AVIANCA|AW|AWS|AX|AXA|AZ|AZURE|BA|BABY|BAIDU|BANAMEX|BANANAREPUBLIC|BAND|BANK|BAR|BARCELONA|BARCLAYCARD|BARCLAYS|BAREFOOT|BARGAINS|BASEBALL|BASKETBALL|BAUHAUS|BAYERN|BB|BBC|BBT|BBVA|BCG|BCN|BD|BE|BEATS|BEAUTY|BEER|BENTLEY|BERLIN|BEST|BESTBUY|BET|BF|BG|BH|BHARTI|BI|BIBLE|BID|BIKE|BING|BINGO|BIO|BIZ|BJ|BLACK|BLACKFRIDAY|BLOCKBUSTER|BLOG|BLOOMBERG|BLUE|BM|BMS|BMW|BN|BNPPARIBAS|BO|BOATS|BOEHRINGER|BOFA|BOM|BOND|BOO|BOOK|BOOKING|BOSCH|BOSTIK|BOSTON|BOT|BOUTIQUE|BOX|BR|BRADESCO|BRIDGESTONE|BROADWAY|BROKER|BROTHER|BRUSSELS|BS|BT|BUDAPEST|BUGATTI|BUILD|BUILDERS|BUSINESS|BUY|BUZZ|BV|BW|BY|BZ|BZH|CA|CAB|CAFE|CAL|CALL|CALVINKLEIN|CAM|CAMERA|CAMP|CANCERRESEARCH|CANON|CAPETOWN|CAPITAL|CAPITALONE|CAR|CARAVAN|CARDS|CARE|CAREER|CAREERS|CARS|CASA|CASE|CASH|CASINO|CAT|CATERING|CATHOLIC|CBA|CBN|CBRE|CBS|CC|CD|CENTER|CEO|CERN|CF|CFA|CFD|CG|CH|CHANEL|CHANNEL|CHARITY|CHASE|CHAT|CHEAP|CHINTAI|CHRISTMAS|CHROME|CHURCH|CI|CIPRIANI|CIRCLE|CISCO|CITADEL|CITI|CITIC|CITY|CITYEATS|CK|CL|CLAIMS|CLEANING|CLICK|CLINIC|CLINIQUE|CLOTHING|CLOUD|CLUB|CLUBMED|CM|CN|CO|COACH|CODES|COFFEE|COLLEGE|COLOGNE|COM|COMCAST|COMMBANK|COMMUNITY|COMPANY|COMPARE|COMPUTER|COMSEC|CONDOS|CONSTRUCTION|CONSULTING|CONTACT|CONTRACTORS|COOKING|COOKINGCHANNEL|COOL|COOP|CORSICA|COUNTRY|COUPON|COUPONS|COURSES|CPA|CR|CREDIT|CREDITCARD|CREDITUNION|CRICKET|CROWN|CRS|CRUISE|CRUISES|CSC|CU|CUISINELLA|CV|CW|CX|CY|CYMRU|CYOU|CZ|DABUR|DAD|DANCE|DATA|DATE|DATING|DATSUN|DAY|DCLK|DDS|DE|DEAL|DEALER|DEALS|DEGREE|DELIVERY|DELL|DELOITTE|DELTA|DEMOCRAT|DENTAL|DENTIST|DESI|DESIGN|DEV|DHL|DIAMONDS|DIET|DIGITAL|DIRECT|DIRECTORY|DISCOUNT|DISCOVER|DISH|DIY|DJ|DK|DM|DNP|DO|DOCS|DOCTOR|DOG|DOMAINS|DOT|DOWNLOAD|DRIVE|DTV|DUBAI|DUCK|DUNLOP|DUPONT|DURBAN|DVAG|DVR|DZ|EARTH|EAT|EC|ECO|EDEKA|EDU|EDUCATION|EE|EG|EMAIL|EMERCK|ENERGY|ENGINEER|ENGINEERING|ENTERPRISES|EPSON|EQUIPMENT|ER|ERICSSON|ERNI|ES|ESQ|ESTATE|ET|ETISALAT|EU|EUROVISION|EUS|EVENTS|EXCHANGE|EXPERT|EXPOSED|EXPRESS|EXTRASPACE|FAGE|FAIL|FAIRWINDS|FAITH|FAMILY|FAN|FANS|FARM|FARMERS|FASHION|FAST|FEDEX|FEEDBACK|FERRARI|FERRERO|FI|FIAT|FIDELITY|FIDO|FILM|FINAL|FINANCE|FINANCIAL|FIRE|FIRESTONE|FIRMDALE|FISH|FISHING|FIT|FITNESS|FJ|FK|FLICKR|FLIGHTS|FLIR|FLORIST|FLOWERS|FLY|FM|FO|FOO|FOOD|FOODNETWORK|FOOTBALL|FORD|FOREX|FORSALE|FORUM|FOUNDATION|FOX|FR|FREE|FRESENIUS|FRL|FROGANS|FRONTDOOR|FRONTIER|FTR|FUJITSU|FUJIXEROX|FUN|FUND|FURNITURE|FUTBOL|FYI|GA|GAL|GALLERY|GALLO|GALLUP|GAME|GAMES|GAP|GARDEN|GAY|GB|GBIZ|GD|GDN|GE|GEA|GENT|GENTING|GEORGE|GF|GG|GGEE|GH|GI|GIFT|GIFTS|GIVES|GIVING|GL|GLADE|GLASS|GLE|GLOBAL|GLOBO|GM|GMAIL|GMBH|GMO|GMX|GN|GODADDY|GOLD|GOLDPOINT|GOLF|GOO|GOODYEAR|GOOG|GOOGLE|GOP|GOT|GOV|GP|GQ|GR|GRAINGER|GRAPHICS|GRATIS|GREEN|GRIPE|GROCERY|GROUP|GS|GT|GU|GUARDIAN|GUCCI|GUGE|GUIDE|GUITARS|GURU|GW|GY|HAIR|HAMBURG|HANGOUT|HAUS|HBO|HDFC|HDFCBANK|HEALTH|HEALTHCARE|HELP|HELSINKI|HERE|HERMES|HGTV|HIPHOP|HISAMITSU|HITACHI|HIV|HK|HKT|HM|HN|HOCKEY|HOLDINGS|HOLIDAY|HOMEDEPOT|HOMEGOODS|HOMES|HOMESENSE|HONDA|HORSE|HOSPITAL|HOST|HOSTING|HOT|HOTELES|HOTELS|HOTMAIL|HOUSE|HOW|HR|HSBC|HT|HU|HUGHES|HYATT|HYUNDAI|IBM|ICBC|ICE|ICU|ID|IE|IEEE|IFM|IKANO|IL|IM|IMAMAT|IMDB|IMMO|IMMOBILIEN|IN|INC|INDUSTRIES|INFINITI|INFO|ING|INK|INSTITUTE|INSURANCE|INSURE|INT|INTERNATIONAL|INTUIT|INVESTMENTS|IO|IPIRANGA|IQ|IR|IRISH|IS|ISMAILI|IST|ISTANBUL|IT|ITAU|ITV|IVECO|JAGUAR|JAVA|JCB|JE|JEEP|JETZT|JEWELRY|JIO|JLL|JM|JMP|JNJ|JO|JOBS|JOBURG|JOT|JOY|JP|JPMORGAN|JPRS|JUEGOS|JUNIPER|KAUFEN|KDDI|KE|KERRYHOTELS|KERRYLOGISTICS|KERRYPROPERTIES|KFH|KG|KH|KI|KIA|KIM|KINDER|KINDLE|KITCHEN|KIWI|KM|KN|KOELN|KOMATSU|KOSHER|KP|KPMG|KPN|KR|KRD|KRED|KUOKGROUP|KW|KY|KYOTO|KZ|LA|LACAIXA|LAMBORGHINI|LAMER|LANCASTER|LANCIA|LAND|LANDROVER|LANXESS|LASALLE|LAT|LATINO|LATROBE|LAW|LAWYER|LB|LC|LDS|LEASE|LECLERC|LEFRAK|LEGAL|LEGO|LEXUS|LGBT|LI|LIDL|LIFE|LIFEINSURANCE|LIFESTYLE|LIGHTING|LIKE|LILLY|LIMITED|LIMO|LINCOLN|LINDE|LINK|LIPSY|LIVE|LIVING|LIXIL|LK|LLC|LLP|LOAN|LOANS|LOCKER|LOCUS|LOFT|LOL|LONDON|LOTTE|LOTTO|LOVE|LPL|LPLFINANCIAL|LR|LS|LT|LTD|LTDA|LU|LUNDBECK|LUXE|LUXURY|LV|LY|MA|MACYS|MADRID|MAIF|MAISON|MAKEUP|MAN|MANAGEMENT|MANGO|MAP|MARKET|MARKETING|MARKETS|MARRIOTT|MARSHALLS|MASERATI|MATTEL|MBA|MC|MCKINSEY|MD|ME|MED|MEDIA|MEET|MELBOURNE|MEME|MEMORIAL|MEN|MENU|MERCKMSD|MG|MH|MIAMI|MICROSOFT|MIL|MINI|MINT|MIT|MITSUBISHI|MK|ML|MLB|MLS|MM|MMA|MN|MO|MOBI|MOBILE|MODA|MOE|MOI|MOM|MONASH|MONEY|MONSTER|MORMON|MORTGAGE|MOSCOW|MOTO|MOTORCYCLES|MOV|MOVIE|MP|MQ|MR|MS|MSD|MT|MTN|MTR|MU|MUSEUM|MUTUAL|MV|MW|MX|MY|MZ|NA|NAB|NAGOYA|NAME|NATIONWIDE|NATURA|NAVY|NBA|NC|NE|NEC|NET|NETBANK|NETFLIX|NETWORK|NEUSTAR|NEW|NEWS|NEXT|NEXTDIRECT|NEXUS|NF|NFL|NG|NGO|NHK|NI|NICO|NIKE|NIKON|NINJA|NISSAN|NISSAY|NL|NO|NOKIA|NORTHWESTERNMUTUAL|NORTON|NOW|NOWRUZ|NOWTV|NP|NR|NRA|NRW|NTT|NU|NYC|NZ|OBI|OBSERVER|OFF|OFFICE|OKINAWA|OLAYAN|OLAYANGROUP|OLDNAVY|OLLO|OM|OMEGA|ONE|ONG|ONL|ONLINE|ONYOURSIDE|OOO|OPEN|ORACLE|ORANGE|ORG|ORGANIC|ORIGINS|OSAKA|OTSUKA|OTT|OVH|PA|PAGE|PANASONIC|PARIS|PARS|PARTNERS|PARTS|PARTY|PASSAGENS|PAY|PCCW|PE|PET|PF|PFIZER|PG|PH|PHARMACY|PHD|PHILIPS|PHONE|PHOTO|PHOTOGRAPHY|PHOTOS|PHYSIO|PICS|PICTET|PICTURES|PID|PIN|PING|PINK|PIONEER|PIZZA|PK|PL|PLACE|PLAY|PLAYSTATION|PLUMBING|PLUS|PM|PN|PNC|POHL|POKER|POLITIE|PORN|POST|PR|PRAMERICA|PRAXI|PRESS|PRIME|PRO|PROD|PRODUCTIONS|PROF|PROGRESSIVE|PROMO|PROPERTIES|PROPERTY|PROTECTION|PRU|PRUDENTIAL|PS|PT|PUB|PW|PWC|PY|QA|QPON|QUEBEC|QUEST|QVC|RACING|RADIO|RAID|RE|READ|REALESTATE|REALTOR|REALTY|RECIPES|RED|REDSTONE|REDUMBRELLA|REHAB|REISE|REISEN|REIT|RELIANCE|REN|RENT|RENTALS|REPAIR|REPORT|REPUBLICAN|REST|RESTAURANT|REVIEW|REVIEWS|REXROTH|RICH|RICHARDLI|RICOH|RIL|RIO|RIP|RMIT|RO|ROCHER|ROCKS|RODEO|ROGERS|ROOM|RS|RSVP|RU|RUGBY|RUHR|RUN|RW|RWE|RYUKYU|SA|SAARLAND|SAFE|SAFETY|SAKURA|SALE|SALON|SAMSCLUB|SAMSUNG|SANDVIK|SANDVIKCOROMANT|SANOFI|SAP|SARL|SAS|SAVE|SAXO|SB|SBI|SBS|SC|SCA|SCB|SCHAEFFLER|SCHMIDT|SCHOLARSHIPS|SCHOOL|SCHULE|SCHWARZ|SCIENCE|SCJOHNSON|SCOT|SD|SE|SEARCH|SEAT|SECURE|SECURITY|SEEK|SELECT|SENER|SERVICES|SES|SEVEN|SEW|SEX|SEXY|SFR|SG|SH|SHANGRILA|SHARP|SHAW|SHELL|SHIA|SHIKSHA|SHOES|SHOP|SHOPPING|SHOUJI|SHOW|SHOWTIME|SI|SILK|SINA|SINGLES|SITE|SJ|SK|SKI|SKIN|SKY|SKYPE|SL|SLING|SM|SMART|SMILE|SN|SNCF|SO|SOCCER|SOCIAL|SOFTBANK|SOFTWARE|SOHU|SOLAR|SOLUTIONS|SONG|SONY|SOY|SPA|SPACE|SPORT|SPOT|SPREADBETTING|SR|SRL|SS|ST|STADA|STAPLES|STAR|STATEBANK|STATEFARM|STC|STCGROUP|STOCKHOLM|STORAGE|STORE|STREAM|STUDIO|STUDY|STYLE|SU|SUCKS|SUPPLIES|SUPPLY|SUPPORT|SURF|SURGERY|SUZUKI|SV|SWATCH|SWIFTCOVER|SWISS|SX|SY|SYDNEY|SYSTEMS|SZ|TAB|TAIPEI|TALK|TAOBAO|TARGET|TATAMOTORS|TATAR|TATTOO|TAX|TAXI|TC|TCI|TD|TDK|TEAM|TECH|TECHNOLOGY|TEL|TEMASEK|TENNIS|TEVA|TF|TG|TH|THD|THEATER|THEATRE|TIAA|TICKETS|TIENDA|TIFFANY|TIPS|TIRES|TIROL|TJ|TJMAXX|TJX|TK|TKMAXX|TL|TM|TMALL|TN|TO|TODAY|TOKYO|TOOLS|TOP|TORAY|TOSHIBA|TOTAL|TOURS|TOWN|TOYOTA|TOYS|TR|TRADE|TRADING|TRAINING|TRAVEL|TRAVELCHANNEL|TRAVELERS|TRAVELERSINSURANCE|TRUST|TRV|TT|TUBE|TUI|TUNES|TUSHU|TV|TVS|TW|TZ|UA|UBANK|UBS|UG|UK|UNICOM|UNIVERSITY|UNO|UOL|UPS|US|UY|UZ|VA|VACATIONS|VANA|VANGUARD|VC|VE|VEGAS|VENTURES|VERISIGN|VERSICHERUNG|VET|VG|VI|VIAJES|VIDEO|VIG|VIKING|VILLAS|VIN|VIP|VIRGIN|VISA|VISION|VIVA|VIVO|VLAANDEREN|VN|VODKA|VOLKSWAGEN|VOLVO|VOTE|VOTING|VOTO|VOYAGE|VU|VUELOS|WALES|WALMART|WALTER|WANG|WANGGOU|WATCH|WATCHES|WEATHER|WEATHERCHANNEL|WEBCAM|WEBER|WEBSITE|WED|WEDDING|WEIBO|WEIR|WF|WHOSWHO|WIEN|WIKI|WILLIAMHILL|WIN|WINDOWS|WINE|WINNERS|WME|WOLTERSKLUWER|WOODSIDE|WORK|WORKS|WORLD|WOW|WS|WTC|WTF|XBOX|XEROX|XFINITY|XIHUAN|XIN|XN--11B4C3D|XN--1CK2E1B|XN--1QQW23A|XN--2SCRJ9C|XN--30RR7Y|XN--3BST00M|XN--3DS443G|XN--3E0B707E|XN--3HCRJ9C|XN--3OQ18VL8PN36A|XN--3PXU8K|XN--42C2D9A|XN--45BR5CYL|XN--45BRJ9C|XN--45Q11C|XN--4DBRK0CE|XN--4GBRIM|XN--54B7FTA0CC|XN--55QW42G|XN--55QX5D|XN--5SU34J936BGSG|XN--5TZM5G|XN--6FRZ82G|XN--6QQ986B3XL|XN--80ADXHKS|XN--80AO21A|XN--80AQECDR1A|XN--80ASEHDB|XN--80ASWG|XN--8Y0A063A|XN--90A3AC|XN--90AE|XN--90AIS|XN--9DBQ2A|XN--9ET52U|XN--9KRT00A|XN--B4W605FERD|XN--BCK1B9A5DRE4C|XN--C1AVG|XN--C2BR7G|XN--CCK2B3B|XN--CCKWCXETD|XN--CG4BKI|XN--CLCHC0EA0B2G2A9GCD|XN--CZR694B|XN--CZRS0T|XN--CZRU2D|XN--D1ACJ3B|XN--D1ALF|XN--E1A4C|XN--ECKVDTC9D|XN--EFVY88H|XN--FCT429K|XN--FHBEI|XN--FIQ228C5HS|XN--FIQ64B|XN--FIQS8S|XN--FIQZ9S|XN--FJQ720A|XN--FLW351E|XN--FPCRJ9C3D|XN--FZC2C9E2C|XN--FZYS8D69UVGM|XN--G2XX48C|XN--GCKR3F0F|XN--GECRJ9C|XN--GK3AT1E|XN--H2BREG3EVE|XN--H2BRJ9C|XN--H2BRJ9C8C|XN--HXT814E|XN--I1B6B1A6A2E|XN--IMR513N|XN--IO0A7I|XN--J1AEF|XN--J1AMH|XN--J6W193G|XN--JLQ480N2RG|XN--JLQ61U9W7B|XN--JVR189M|XN--KCRX77D1X4A|XN--KPRW13D|XN--KPRY57D|XN--KPUT3I|XN--L1ACC|XN--LGBBAT1AD8J|XN--MGB9AWBF|XN--MGBA3A3EJT|XN--MGBA3A4F16A|XN--MGBA7C0BBN0A|XN--MGBAAKC7DVF|XN--MGBAAM7A8H|XN--MGBAB2BD|XN--MGBAH1A3HJKRD|XN--MGBAI9AZGQP6J|XN--MGBAYH7GPA|XN--MGBBH1A|XN--MGBBH1A71E|XN--MGBC0A9AZCG|XN--MGBCA7DZDO|XN--MGBCPQ6GPA1A|XN--MGBERP4A5D4AR|XN--MGBGU82A|XN--MGBI4ECEXP|XN--MGBPL2FH|XN--MGBT3DHD|XN--MGBTX2B|XN--MGBX4CD0AB|XN--MIX891F|XN--MK1BU44C|XN--MXTQ1M|XN--NGBC5AZD|XN--NGBE9E0A|XN--NGBRX|XN--NODE|XN--NQV7F|XN--NQV7FS00EMA|XN--NYQY26A|XN--O3CW4H|XN--OGBPF8FL|XN--OTU796D|XN--P1ACF|XN--P1AI|XN--PGBS0DH|XN--PSSY2U|XN--Q7CE6A|XN--Q9JYB4C|XN--QCKA1PMC|XN--QXA6A|XN--QXAM|XN--RHQV96G|XN--ROVU88B|XN--RVC1E0AM3E|XN--S9BRJ9C|XN--SES554G|XN--T60B56A|XN--TCKWE|XN--TIQ49XQYJ|XN--UNUP4Y|XN--VERMGENSBERATER-CTB|XN--VERMGENSBERATUNG-PWB|XN--VHQUV|XN--VUQ861B|XN--W4R85EL8FHU5DNRA|XN--W4RS40L|XN--WGBH1C|XN--WGBL6A|XN--XHQ521B|XN--XKC2AL3HYE2A|XN--XKC2DL3A5EE0H|XN--Y9A3AQ|XN--YFRO4I67O|XN--YGBI2AMMX|XN--ZFR164B|XXX|XYZ|YACHTS|YAHOO|YAMAXUN|YANDEX|YE|YODOBASHI|YOGA|YOKOHAMA|YOU|YOUTUBE|YT|YUN|ZA|ZAPPOS|ZARA|ZERO|ZIP|ZM|ZONE|ZUERICH|ZW)$/i; 24 | 25 | var proxy; 26 | 27 | /** 28 | * @param req_url {string} The requested URL (scheme is optional). 29 | * @return {object} URL parsed using url.parse 30 | */ 31 | function parseURL(req_url, logger) { 32 | var match = req_url.match(/^(?:(https?:)?\/\/)?(([^\/?]+?)(?::(\d{0,5})(?=[\/?]|$))?)([\/?][\S\s]*|$)/i); 33 | // ^^^^^^^ ^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^ 34 | // 1:protocol 3:hostname 4:port 5:path + query string 35 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 36 | // 2:host 37 | if (!match) { 38 | return null; 39 | } 40 | if (!match[1]) { 41 | if (/^https?:/i.test(req_url)) { 42 | // The pattern at top could mistakenly parse "http:///" as host="http:" and path=///. 43 | return null; 44 | } 45 | // Scheme is omitted. 46 | if (req_url.lastIndexOf('//', 0) === -1) { 47 | // "//" is omitted. 48 | req_url = '//' + req_url; 49 | } 50 | let scheme = 'https:'; 51 | if ((match[4] !== '443') && (match[4] !== '1280')) { 52 | scheme = 'http:'; 53 | } 54 | req_url = scheme + req_url; 55 | } 56 | var parsed = url.parse(req_url); 57 | if (!parsed.hostname) { 58 | return null; 59 | } 60 | return parsed; 61 | } 62 | 63 | 64 | /** 65 | * Check whether the specified hostname is valid. 66 | * 67 | * @param hostname {string} Host name (excluding port) of requested resource. 68 | * @return {boolean} Whether the requested resource can be accessed. 69 | */ 70 | function isValidHostName(hostname) { 71 | return !!( 72 | regexp_tld.test(hostname) || 73 | net.isIPv4(hostname) || 74 | net.isIPv6(hostname) 75 | ); 76 | } 77 | 78 | 79 | /** 80 | * Adds CORS headers to the response headers. 81 | * 82 | * @param headers {object} Response headers 83 | * @param request {ServerRequest} 84 | */ 85 | function withCORS(headers, request) { 86 | headers['access-control-allow-origin'] = common.generateAccessControlAllowOrigin(request); 87 | headers['access-control-allow-credentials'] = 'true'; 88 | var corsMaxAge = request.corsAnywhereRequestState.corsMaxAge; 89 | if (request.method === 'OPTIONS' && corsMaxAge) { 90 | headers['access-control-max-age'] = corsMaxAge; 91 | } 92 | if (request.headers['access-control-request-method']) { 93 | headers['access-control-allow-methods'] = request.headers['access-control-request-method']; 94 | delete request.headers['access-control-request-method']; 95 | } 96 | if (request.headers['access-control-request-headers']) { 97 | headers['access-control-allow-headers'] = request.headers['access-control-request-headers']; 98 | delete request.headers['access-control-request-headers']; 99 | } 100 | 101 | headers['access-control-expose-headers'] = Object.keys(headers).join(','); 102 | 103 | return headers; 104 | } 105 | 106 | 107 | /** 108 | * This method modifies the response headers of the proxied response. 109 | * If a redirect is detected, the response is not sent to the client, 110 | * and a new request is initiated. 111 | * 112 | * client (req) -> CORS Anywhere -> (proxyReq) -> other server 113 | * client (res) <- CORS Anywhere <- (proxyRes) <- other server 114 | * 115 | * @param proxy {HttpProxy} 116 | * @param proxyReq {ClientRequest} The outgoing request to the other server. 117 | * @param proxyRes {ServerResponse} The response from the other server. 118 | * @param req {IncomingMessage} Incoming HTTP request, augmented with property corsAnywhereRequestState 119 | * @param req.corsAnywhereRequestState {object} 120 | * @param req.corsAnywhereRequestState.location {object} See parseURL 121 | * @param req.corsAnywhereRequestState.getProxyForUrl {function} See proxyRequest 122 | * @param req.corsAnywhereRequestState.proxyBaseUrl {string} Base URL of the CORS API endpoint 123 | * @param req.corsAnywhereRequestState.maxRedirects {number} Maximum number of redirects 124 | * @param req.corsAnywhereRequestState.redirectCount_ {number} Internally used to count redirects 125 | * @param res {ServerResponse} Outgoing response to the client that wanted to proxy the HTTP request. 126 | * 127 | * @returns {boolean} true if http-proxy should continue to pipe proxyRes to res. 128 | */ 129 | function onProxyResponse(proxy, proxyReq, proxyRes, req, res) { 130 | var requestState = req.corsAnywhereRequestState; 131 | 132 | var statusCode = proxyRes.statusCode; 133 | 134 | if (!requestState.redirectCount_) { 135 | res.setHeader('x-request-url', requestState.location.href); 136 | } 137 | // Handle redirects 138 | if (statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308) { 139 | var locationHeader = proxyRes.headers.location; 140 | var parsedLocation; 141 | if (locationHeader) { 142 | locationHeader = url.resolve(requestState.location.href, locationHeader); 143 | parsedLocation = parseURL(locationHeader); 144 | } 145 | if (parsedLocation) { 146 | if (statusCode === 301 || statusCode === 302 || statusCode === 303) { 147 | // Exclude 307 & 308, because they are rare, and require preserving the method + request body 148 | requestState.redirectCount_ = requestState.redirectCount_ + 1 || 1; 149 | if (requestState.redirectCount_ <= requestState.maxRedirects) { 150 | // Handle redirects within the server, because some clients (e.g. Android Stock Browser) 151 | // cancel redirects. 152 | // Set header for debugging purposes. Do not try to parse it! 153 | res.setHeader('X-CORS-Redirect-' + requestState.redirectCount_, statusCode + ' ' + locationHeader); 154 | 155 | req.method = 'GET'; 156 | req.headers['content-length'] = '0'; 157 | delete req.headers['content-type']; 158 | requestState.location = parsedLocation; 159 | 160 | // Remove all listeners (=reset events to initial state) 161 | req.removeAllListeners(); 162 | 163 | // Remove the error listener so that the ECONNRESET "error" that 164 | // may occur after aborting a request does not propagate to res. 165 | // https://github.com/nodejitsu/node-http-proxy/blob/v1.11.1/lib/http-proxy/passes/web-incoming.js#L134 166 | proxyReq.removeAllListeners('error'); 167 | proxyReq.once('error', function catchAndIgnoreError() {}); 168 | proxyReq.abort(); 169 | 170 | // Initiate a new proxy request. 171 | proxyRequest(req, res, proxy); 172 | return false; 173 | } 174 | } 175 | proxyRes.headers.location = requestState.proxyBaseUrl + '/' + locationHeader; 176 | } 177 | } 178 | 179 | // Strip cookies 180 | // delete proxyRes.headers['set-cookie']; 181 | // delete proxyRes.headers['set-cookie2']; 182 | 183 | proxyRes.headers['x-final-url'] = requestState.location.href; 184 | withCORS(proxyRes.headers, req); 185 | return true; 186 | } 187 | 188 | 189 | /** 190 | * Performs the actual proxy request. 191 | * 192 | * @param req {ServerRequest} Incoming http request 193 | * @param res {ServerResponse} Outgoing (proxied) http request 194 | * @param proxy {HttpProxy} 195 | */ 196 | function proxyRequest(req, res, proxy) { 197 | 198 | var location = req.corsAnywhereRequestState.location; 199 | req.url = location.path; 200 | 201 | var proxyOptions = { 202 | changeOrigin: false, 203 | prependPath: false, 204 | target: location, 205 | headers: { 206 | host: location.host, 207 | }, 208 | buffer: { 209 | pipe: function(proxyReq) { 210 | var proxyReqOn = proxyReq.on; 211 | proxyReq.on = function(eventName, listener) { 212 | if (eventName !== 'response') { 213 | return proxyReqOn.call(this, eventName, listener); 214 | } 215 | return proxyReqOn.call(this, 'response', function(proxyRes) { 216 | if (onProxyResponse(proxy, proxyReq, proxyRes, req, res)) { 217 | try { 218 | listener(proxyRes); 219 | } catch (err) { 220 | proxyReq.emit('error', err); 221 | } 222 | } 223 | }); 224 | }; 225 | return req.pipe(proxyReq); 226 | }, 227 | }, 228 | }; 229 | 230 | // Start proxying the request 231 | try { 232 | proxy.web(req, res, proxyOptions); 233 | } catch (err) { 234 | proxy.emit('error', err, req, res); 235 | } 236 | } 237 | 238 | 239 | corsProxy.createProxy = function createProxy(options) { 240 | 241 | if (typeof proxy === 'undefined') { 242 | 243 | options = options || {}; 244 | 245 | // Default options: 246 | var httpProxyOptions = { 247 | xfwd: true, // Append X-Forwarded-* headers 248 | secure: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0', 249 | }; 250 | // Allow override defaults and add own options 251 | if (options.httpProxyOptions) { 252 | Object.keys(options.httpProxyOptions).forEach(function(option) { 253 | httpProxyOptions[option] = options.httpProxyOptions[option]; 254 | }); 255 | } 256 | 257 | proxy = httpProxy.createServer(httpProxyOptions); 258 | } 259 | 260 | return proxy; 261 | } 262 | 263 | 264 | corsProxy.getHandler = function getHandler(options, proxy) { 265 | 266 | var corsAnywhere = { 267 | maxRedirects: 5, // Maximum number of redirects to be followed. 268 | originBlacklist: [], // Requests from these origins will be blocked. 269 | originWhitelist: [], // If non-empty, requests not from an origin in this list will be blocked. 270 | redirectSameOrigin: false, // Redirect the client to the requested URL for same-origin requests. 271 | requireHeader: null, // Require a header to be set? 272 | removeHeaders: [], // Strip these request headers. 273 | setHeaders: {}, // Set these request headers. 274 | corsMaxAge: 0, // If set, an Access-Control-Max-Age header with this value (in seconds) will be added. 275 | logger: null, // logger 276 | }; 277 | 278 | Object.keys(corsAnywhere).forEach(function(option) { 279 | if (Object.prototype.hasOwnProperty.call(options, option)) { 280 | corsAnywhere[option] = options[option]; 281 | } 282 | }); 283 | 284 | // Convert corsAnywhere.requireHeader to an array of lowercase header names, or null. 285 | if (corsAnywhere.requireHeader) { 286 | if (typeof corsAnywhere.requireHeader === 'string') { 287 | corsAnywhere.requireHeader = [corsAnywhere.requireHeader.toLowerCase()]; 288 | } else if (!Array.isArray(corsAnywhere.requireHeader) || corsAnywhere.requireHeader.length === 0) { 289 | corsAnywhere.requireHeader = null; 290 | } else { 291 | corsAnywhere.requireHeader = corsAnywhere.requireHeader.map(function(headerName) { 292 | return headerName.toLowerCase(); 293 | }); 294 | } 295 | } 296 | var hasRequiredHeaders = function(headers) { 297 | return !corsAnywhere.requireHeader || corsAnywhere.requireHeader.some(function(headerName) { 298 | return Object.hasOwnProperty.call(headers, headerName); 299 | }); 300 | }; 301 | 302 | return function(req, res, logger) { 303 | 304 | logger.silly('corsProxy handler entered for req.url [%o]', req.url); 305 | 306 | req.corsAnywhereRequestState = { 307 | maxRedirects: corsAnywhere.maxRedirects, 308 | corsMaxAge: corsAnywhere.corsMaxAge, 309 | }; 310 | 311 | var cors_headers = withCORS({}, req); 312 | if (req.method === 'OPTIONS') { 313 | // Pre-flight request. Reply successfully: 314 | logger.silly('corsProxy handler method is OPTIONS'); 315 | res.writeHead(200, cors_headers); 316 | res.end(); 317 | return; 318 | } 319 | 320 | var location = parseURL(req.url.slice(17), logger); 321 | 322 | // if (!/^\/https?:/.test(req.url) && !isValidHostName(location.hostname)) { 323 | // // Don't even try to proxy invalid hosts (such as /favicon.ico, /robots.txt) 324 | // res.writeHead(404, 'Invalid host', cors_headers); 325 | // res.end('Invalid host: ' + location.hostname); 326 | // return; 327 | // } 328 | 329 | if (!hasRequiredHeaders(req.headers)) { 330 | res.writeHead(400, 'Header required', cors_headers); 331 | res.end('Missing required request header. Must specify one of: ' + corsAnywhere.requireHeader); 332 | return; 333 | } 334 | 335 | var origin = req.headers.origin || ''; 336 | if (corsAnywhere.originBlacklist.indexOf(origin) >= 0) { 337 | res.writeHead(403, 'Forbidden', cors_headers); 338 | res.end('The origin "' + origin + '" was blacklisted by the operator of this proxy.'); 339 | return; 340 | } 341 | 342 | if (corsAnywhere.originWhitelist.length && corsAnywhere.originWhitelist.indexOf(origin) === -1) { 343 | res.writeHead(403, 'Forbidden', cors_headers); 344 | res.end('The origin "' + origin + '" was not whitelisted by the operator of this proxy.'); 345 | return; 346 | } 347 | 348 | if (corsAnywhere.redirectSameOrigin && origin && location.href[origin.length] === '/' && location.href.lastIndexOf(origin, 0) === 0) { 349 | // Send a permanent redirect to offload the server. Badly coded clients should not waste our resources. 350 | cors_headers.vary = 'origin'; 351 | cors_headers['cache-control'] = 'private'; 352 | cors_headers.location = location.href; 353 | res.writeHead(301, 'Please use a direct request', cors_headers); 354 | res.end(); 355 | return; 356 | } 357 | 358 | var isRequestedOverHttps = req.connection.encrypted || /^\s*https/.test(req.headers['x-forwarded-proto']); 359 | var proxyBaseUrl = (isRequestedOverHttps ? 'https://' : 'http://') + req.headers.host; 360 | 361 | corsAnywhere.removeHeaders.forEach(function(header) { 362 | delete req.headers[header]; 363 | }); 364 | 365 | Object.keys(corsAnywhere.setHeaders).forEach(function(header) { 366 | req.headers[header] = corsAnywhere.setHeaders[header]; 367 | }); 368 | 369 | req.corsAnywhereRequestState.location = location; 370 | req.corsAnywhereRequestState.proxyBaseUrl = proxyBaseUrl; 371 | 372 | proxyRequest(req, res, proxy); 373 | }; 374 | } 375 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/urlon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var keyStringifyRegexp = /([=:@$/])/g 4 | var valueStringifyRegexp = /([&;/])/g 5 | var keyParseRegexp = /[=:@$]/ 6 | var valueParseRegexp = /[&;]/ 7 | 8 | function encodeString(str, regexp) { 9 | return encodeURI(str.replace(regexp, '/$1')) 10 | } 11 | 12 | function trim(res) { 13 | return typeof res === 'string' ? res.replace(/;+$/g, '') : res 14 | } 15 | 16 | function stringify(input, recursive) { 17 | if (!recursive) { 18 | return trim(stringify(input, true)) 19 | } 20 | // Number, Boolean or Null 21 | if ( 22 | typeof input === 'number' || 23 | input === true || 24 | input === false || 25 | input === null 26 | ) { 27 | return ':' + input 28 | } 29 | var res = [] 30 | // Array 31 | if (input instanceof Array) { 32 | for (var i = 0; i < input.length; ++i) { 33 | typeof input[i] === 'undefined' 34 | ? res.push(':null') 35 | : res.push(stringify(input[i], true)) 36 | } 37 | return '@' + res.join('&') + ';' 38 | } 39 | // Object 40 | if (typeof input === 'object') { 41 | for (var key in input) { 42 | var val = stringify(input[key], true) 43 | if (val) { 44 | res.push(encodeString(key, keyStringifyRegexp) + val) 45 | } 46 | } 47 | return '$' + res.join('&') + ';' 48 | } 49 | // undefined 50 | if (typeof input === 'undefined') { 51 | return 52 | } 53 | // String 54 | return '=' + encodeString(input.toString(), valueStringifyRegexp) 55 | } 56 | 57 | function parse(str) { 58 | var pos = 0 59 | str = decodeURI(str) 60 | 61 | function readToken(regexp) { 62 | var token = '' 63 | for (; pos !== str.length; ++pos) { 64 | if (str.charAt(pos) === '/') { 65 | pos += 1 66 | if (pos === str.length) { 67 | token += ';' 68 | break 69 | } 70 | } else if (str.charAt(pos).match(regexp)) { 71 | break 72 | } 73 | token += str.charAt(pos) 74 | } 75 | return token 76 | } 77 | 78 | function parseToken() { 79 | var type = str.charAt(pos++) 80 | // String 81 | if (type === '=') { 82 | return readToken(valueParseRegexp) 83 | } 84 | // Number, Boolean or Null 85 | if (type === ':') { 86 | var value = readToken(valueParseRegexp) 87 | if (value === 'true') { 88 | return true 89 | } 90 | if (value === 'false') { 91 | return false 92 | } 93 | value = parseFloat(value) 94 | return isNaN(value) ? null : value 95 | } 96 | var res 97 | // Array 98 | if (type === '@') { 99 | res = [] 100 | loop: { 101 | // empty array 102 | if (pos >= str.length || str.charAt(pos) === ';') { 103 | break loop 104 | } 105 | // parse array items 106 | while (1) { 107 | res.push(parseToken()) 108 | if (pos >= str.length || str.charAt(pos) === ';') { 109 | break loop 110 | } 111 | pos += 1 112 | } 113 | } 114 | pos += 1 115 | return res 116 | } 117 | // Object 118 | if (type === '$') { 119 | res = {} 120 | loop: { 121 | if (pos >= str.length || str.charAt(pos) === ';') { 122 | break loop 123 | } 124 | while (1) { 125 | var name = readToken(keyParseRegexp) 126 | res[name] = parseToken() 127 | if (pos >= str.length || str.charAt(pos) === ';') { 128 | break loop 129 | } 130 | pos += 1 131 | } 132 | } 133 | pos += 1 134 | return res 135 | } 136 | // Error 137 | throw new Error('Unexpected char ' + type) 138 | } 139 | 140 | return parseToken() 141 | } 142 | 143 | module.exports = { 144 | stringify, 145 | parse, 146 | }; 147 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/web-incoming-zitified.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | var httpNative = require('http'), 19 | httpsNative = require('https'), 20 | pump = require('pump'), 21 | fs = require('fs'), 22 | path = require('path'), 23 | common = require('../common'), 24 | env = require('../../env'), 25 | pjson = require('../../../package.json'); 26 | const { isUndefined, isEqual } = require('lodash'); 27 | corsProxy = require('./cors-proxy'), 28 | url = require('url'), 29 | net = require('net'), 30 | requestIp = require('request-ip'), 31 | { stringify } = require('./urlon'), 32 | cookie = require('cookie'); 33 | 34 | 35 | /*! 36 | * Array of passes. 37 | * 38 | * A `pass` is just a function that is executed on `req, res, options` 39 | * so that you can easily add new checks while still keeping the base 40 | * flexible. 41 | */ 42 | 43 | 44 | module.exports = { 45 | 46 | /** 47 | * Sets `content-length` to '0' if request is of DELETE type. 48 | * 49 | * @param {ClientRequest} Req Request object 50 | * @param {IncomingMessage} Res Response object 51 | * @param {Object} Options Config object passed to the proxy 52 | * 53 | * @api private 54 | */ 55 | 56 | deleteLength: function deleteLength(req, res, options) { 57 | if((req.method === 'DELETE' || req.method === 'OPTIONS') 58 | && !req.headers['content-length']) { 59 | req.headers['content-length'] = '0'; 60 | delete req.headers['transfer-encoding']; 61 | } 62 | }, 63 | 64 | /** 65 | * Sets timeout in request socket if it was specified in options. 66 | * 67 | * @param {ClientRequest} Req Request object 68 | * @param {IncomingMessage} Res Response object 69 | * @param {Object} Options Config object passed to the proxy 70 | * 71 | * @api private 72 | */ 73 | 74 | timeout: function timeout(req, res, options) { 75 | if(options.timeout) { 76 | req.socket.setTimeout(options.timeout); 77 | } 78 | }, 79 | 80 | /** 81 | * Sets `x-forwarded-*` headers if specified in config. 82 | * 83 | * @param {ClientRequest} Req Request object 84 | * @param {IncomingMessage} Res Response object 85 | * @param {Object} Options Config object passed to the proxy 86 | * 87 | * @api private 88 | */ 89 | 90 | XHeaders: function XHeaders(req, res, options) { 91 | if(!options.xfwd) return; 92 | 93 | let encrypted = req.isSpdy || common.hasEncryptedConnection(req); 94 | let values = { 95 | for : req.connection.remoteAddress || req.socket.remoteAddress, 96 | port : common.getPort(req), 97 | proto: encrypted ? 'https' : 'http' 98 | }; 99 | 100 | ['for', 'port', 'proto'].forEach(function(header) { 101 | req.headers['x-forwarded-' + header] = 102 | (req.headers['x-forwarded-' + header] || '') + 103 | (req.headers['x-forwarded-' + header] ? ',' : '') + 104 | values[header]; 105 | }); 106 | 107 | req.headers['x-forwarded-host'] = req.headers['x-forwarded-host'] || req.headers['host'] || ''; 108 | }, 109 | 110 | /** 111 | * Does the actual proxying. If `forward` is enabled fires up 112 | * a ForwardStream, same happens for ProxyStream. The request 113 | * just dies otherwise. 114 | * 115 | * @param {ClientRequest} Req Request object 116 | * @param {IncomingMessage} Res Response object 117 | * @param {Object} Options Config object passed to the proxy 118 | * 119 | * @api private 120 | */ 121 | 122 | stream: async function stream(req, res, options, _, server, clb) { 123 | 124 | // And we begin! 125 | if (!net.isIP(req.headers['host'])) { 126 | options.logger.silly({message: 'req start', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url, hostname: req.hostname}); 127 | } 128 | 129 | // If wildcard vhost support is enabled (i.e. accessing 'zrok shares' ) 130 | if (env('ZITI_BROWZER_BOOTSTRAPPER_WILDCARD_VHOSTS') && env('NOT_YET_IMPLEMENTED')) { 131 | 132 | // Determine if we previously encountered this service/share name... 133 | let zitiContext = await common.getZitiContext(); 134 | let id = await zitiContext.getServiceIdByName( req.ziti_target_service ); 135 | 136 | if (isUndefined(id)) { // if not... 137 | 138 | // Ask the Controller if the service/share exists 139 | let fetchComplete = false; 140 | do { 141 | zitiContext = await common.getZitiContext(); 142 | fetchComplete = await zitiContext.fetchServiceByName( req.ziti_target_service ); 143 | if (!fetchComplete) { 144 | await common.delay(100); 145 | } 146 | } while (!fetchComplete) 147 | 148 | // Does network have the service/share name... 149 | zitiContext = await common.getZitiContext(); 150 | id = await zitiContext.getServiceIdByName( req.ziti_target_service ); 151 | 152 | if (isUndefined(id)) { // if not, route the HTTP request to a 404 response 153 | 154 | let browzer_error_data_json = { 155 | status: 404, 156 | myvar: { 157 | type: 'zrok', 158 | zrokshare: `${req.ziti_target_service}`, 159 | } 160 | }; 161 | 162 | res.writeHead( 301, 163 | { 164 | 'X-Ziti-BrowZer-Bootstrapper': `target zrok share [${req.ziti_target_service}] unknown`, 165 | 'Location': `/browzer_error?browzer_error_data=${stringify(JSON.stringify(browzer_error_data_json))}` 166 | } 167 | ); 168 | res.end(''); 169 | if (!net.isIP(req.headers['host'])) { 170 | options.logger.warn({message: `req terminate - target zrok share [${req.ziti_target_service}] unknown`, vhost: req.headers['host']}); 171 | } 172 | return; 173 | 174 | } 175 | 176 | } 177 | 178 | } 179 | 180 | // 181 | if (typeof req.ziti_target_service === 'undefined') { 182 | res.writeHead(404, { 'X-Ziti-BrowZer-Bootstrapper': `vhost [${req.headers['host']}] unknown` }); 183 | res.end(''); 184 | if (!net.isIP(req.headers['host'])) { 185 | options.logger.error({message: 'req terminate; vhost unknown', vhost: req.headers['host']}); 186 | } 187 | return; 188 | 189 | } 190 | 191 | // 192 | options.target = 'https://' + req.ziti_target_service; 193 | options.targetPath = req.ziti_target_path; 194 | 195 | server.emit('start', req, res, options.target || options.forward); 196 | 197 | let outgoing = common.setupOutgoing(options.ssl || {}, options, req); 198 | 199 | // 200 | // If request is for the naked/un-SHA'ed Ziti BrowZer Runtime 201 | // 202 | let rtNakedRequest = (outgoing.path.match(/\/ziti-browzer-runtime\.js/) || []).length; 203 | if ((rtNakedRequest > 0)) { 204 | options.logger.silly({message: 'Request for naked ziti-browzer-runtime.js', component: 'JS', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 205 | 206 | // Locate the path to the distro within the build of our running instance 207 | let pathToZitiBrowzerRuntimeModule; 208 | pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 209 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 210 | 211 | // Read the component off the disk 212 | let rtFileName = common.getZBRname(); 213 | options.logger.silly({message: 'ziti-browzer-runtime SHA filename: ' + rtFileName, clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 214 | 215 | res.writeHead(302, common.addServerHeader({ 216 | 'Location': `${rtFileName}`, 217 | 'X-Ziti-BrowZer-Bootstrapper': 'ziti-browzer-runtime SHA filename', 218 | })); 219 | res.end(''); 220 | 221 | return; 222 | } 223 | 224 | // 225 | // If request is a Ziti CORS Proxy 226 | // 227 | let corsProxyRequest = (outgoing.path.match(/\/ziti-cors-proxy\//) || []).length; 228 | if ((corsProxyRequest > 0)) { 229 | options.logger.silly({message: 'beginning CORS Proxy', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 230 | 231 | let proxy = corsProxy.createProxy({}); 232 | 233 | let corsRequestHandler = corsProxy.getHandler( 234 | { 235 | logger: options.logger, 236 | }, 237 | proxy); 238 | 239 | corsRequestHandler(req, res, options.logger); 240 | 241 | return; 242 | } 243 | 244 | // Terminate any requests that are not GET's 245 | if (req.method !== 'GET') { 246 | res.writeHead(403, { 'X-Ziti-BrowZer-Bootstrapper': 'non-GET methods are prohibited' }); 247 | res.end(''); 248 | options.logger.warn({message: 'req terminate; non-GET method', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 249 | return; 250 | } 251 | 252 | // 253 | // If request is for resource related to the Ziti BrowZer Runtime 254 | // 255 | let rtRequest = (outgoing.path.match(/\/ziti-browzer-runtime/) || []).length; 256 | if ((rtRequest > 0)) { 257 | options.logger.silly({message: 'Request for ziti-browzer-runtime', component: 'JS', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 258 | 259 | // Locate the path to the SW distro within the build of our running instance 260 | let pathToZitiBrowzerRuntimeModule; 261 | pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 262 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 263 | 264 | // Read the component off the disk 265 | let rtFileName = common.getZBRname(); 266 | fs.readFile( path.join( pathToZitiBrowzerRuntimeModule, rtFileName ), (err, data) => { 267 | 268 | if (err) { // If we can't read the file from disk 269 | 270 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 271 | res.end(''); 272 | return; 273 | 274 | } else { // Emit the file from disk 275 | 276 | var browzer_bootstrapper_host = env('ZITI_BROWZER_BOOTSTRAPPER_HOST'); 277 | 278 | var cacheData = { 279 | status: 200, 280 | headers: common.addServerHeader({ 281 | 'Content-Type': 'application/javascript', 282 | 'Content-Security-Policy': "script-src 'self' " + browzer_bootstrapper_host + " 'unsafe-inline' 'unsafe-eval' blob:; worker-src 'self' 'unsafe-inline' 'unsafe-eval' blob:;", 283 | }), 284 | data: data 285 | } 286 | 287 | res.writeHead(cacheData.status, cacheData.headers); 288 | 289 | res.write(cacheData.data); // the actual file contents 290 | 291 | options.cache.set(req.url, cacheData); 292 | 293 | res.end(); 294 | return; 295 | } 296 | 297 | }); 298 | 299 | return; 300 | } 301 | 302 | // 303 | // If request is for resource related to the Ziti BrowZer CSS 304 | // 305 | let cssRequest = (outgoing.path.match(/\/ziti-browzer-css/) || []).length; 306 | if ((cssRequest > 0)) { 307 | options.logger.silly({message: 'Request for ziti-browzer-css.css', component: 'CSS', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 308 | 309 | // Locate the path to the distro within the build of our running instance 310 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 311 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 312 | 313 | // Read the component off the disk 314 | let cssFileName = common.getZBRCSSname(); 315 | fs.readFile( path.join( pathToZitiBrowzerRuntimeModule, cssFileName ), (err, data) => { 316 | 317 | if (err) { // If we can't read the file from disk 318 | 319 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 320 | res.end(''); 321 | return; 322 | 323 | } else { // Emit the file from disk 324 | 325 | var cacheData = { 326 | status: 200, 327 | headers: common.addServerHeader({ 328 | 'Content-Type': 'text/css', 329 | }), 330 | data: data 331 | } 332 | 333 | res.writeHead(cacheData.status, cacheData.headers); 334 | 335 | res.write(cacheData.data); // the actual file contents 336 | 337 | options.cache.set(req.url, cacheData); 338 | 339 | res.end(); 340 | return; 341 | } 342 | 343 | }); 344 | 345 | return; 346 | } 347 | 348 | // 349 | // If request is for resource related to the Ziti BrowZer cursor 350 | // 351 | let curRequest = (outgoing.path.match(/\/ziti-browzer-cur/) || []).length; 352 | if ((curRequest > 0)) { 353 | options.logger.silly({message: 'Request for ziti-browzer-cur.cur', component: 'CUR', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 354 | 355 | // Locate the path to the distro within the build of our running instance 356 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 357 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 358 | 359 | // Read the component off the disk 360 | let curFileName = 'ziti-browzer-cur.cur'; 361 | fs.readFile( path.join( pathToZitiBrowzerRuntimeModule, curFileName ), (err, data) => { 362 | 363 | if (err) { // If we can't read the file from disk 364 | 365 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 366 | res.end(''); 367 | return; 368 | 369 | } else { // Emit the file from disk 370 | 371 | var cacheData = { 372 | status: 200, 373 | headers: common.addServerHeader({ 374 | 'Content-Type': 'text/css', 375 | }), 376 | data: data 377 | } 378 | 379 | res.writeHead(cacheData.status, cacheData.headers); 380 | 381 | res.write(cacheData.data); // the actual file contents 382 | 383 | options.cache.set(req.url, cacheData); 384 | 385 | res.end(); 386 | return; 387 | } 388 | 389 | }); 390 | 391 | return; 392 | } 393 | 394 | // 395 | // If request is for resource related to the Ziti BrowZer Logo SVG 396 | // 397 | let logoRequest = (outgoing.path.match(/\/ziti-browzer-logo.svg/) || []).length; 398 | if ((logoRequest > 0)) { 399 | options.logger.silly({message: 'Request for ziti-browzer-logo.svg', component: 'SVG', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 400 | 401 | // Locate the path to the distro within the build of our running instance 402 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 403 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 404 | 405 | // Read the component off the disk 406 | fs.readFile( path.join( pathToZitiBrowzerRuntimeModule, 'ziti-browzer-logo.svg' ), (err, data) => { 407 | 408 | if (err) { // If we can't read the file from disk 409 | 410 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 411 | res.end(''); 412 | return; 413 | 414 | } else { // Emit the file from disk 415 | 416 | var cacheData = { 417 | status: 200, 418 | headers: common.addServerHeader({ 419 | 'Content-Type': 'image/svg+xml', 420 | }), 421 | data: data 422 | } 423 | 424 | res.writeHead(cacheData.status, cacheData.headers); 425 | 426 | res.write(cacheData.data); // the actual file contents 427 | 428 | options.cache.set(req.url, cacheData); 429 | 430 | res.end(); 431 | return; 432 | } 433 | 434 | }); 435 | 436 | return; 437 | } 438 | 439 | // 440 | // If request is for resource related to the Ziti BrowZer Runtime's WebAssembly 441 | // 442 | let rtwasmRequest = (outgoing.path.match(/\/libcrypto.*.wasm$/) || []).length; 443 | if ((rtwasmRequest > 0)) { 444 | options.logger.silly({message: 'Request for ziti-browzer-runtime libcrypto.wasm', component: 'WASM', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 445 | 446 | // Locate the path to the SW distro within the build of our running instance 447 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/libcrypto-js'); 448 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 449 | 450 | // Read the component off the disk 451 | let rtwasmFileName = outgoing.path.split("/").pop(); 452 | rtwasmFileName = rtwasmFileName.split("?")[0]; 453 | 454 | fs.readFile( path.join( pathToZitiBrowzerRuntimeModule, rtwasmFileName ), (err, data) => { 455 | 456 | if (err) { // If we can't read the file from disk 457 | 458 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 459 | res.end(''); 460 | return; 461 | 462 | } else { // Emit the file from disk 463 | 464 | var cacheData = { 465 | status: 200, 466 | headers: common.addServerHeader({ 467 | 'Content-Type': 'application/wasm', 468 | 'X-Ziti-BrowZer-Bootstrapper': 'OpenZiti browZer WebAssembly', 469 | }), 470 | data: data 471 | } 472 | 473 | res.writeHead(cacheData.status, cacheData.headers); 474 | 475 | res.write(cacheData.data); // the actual file contents 476 | 477 | options.cache.set(req.url, cacheData); 478 | 479 | res.end(); 480 | return; 481 | } 482 | 483 | }); 484 | 485 | return; 486 | } 487 | 488 | // 489 | // If request is for resource related to Polipop 490 | // 491 | let polipopRequest = (outgoing.path.match(/\/polipop/) || []).length; 492 | if ((polipopRequest > 0)) { 493 | options.logger.silly({message: 'Request for ziti-browzer-UI', component: 'UI', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 494 | 495 | // Locate the path to the distro within the build of our running instance 496 | let pathToZitiBrowzerRuntimeModule = require.resolve('@openziti/ziti-browzer-runtime'); 497 | pathToZitiBrowzerRuntimeModule = pathToZitiBrowzerRuntimeModule.substring(0, pathToZitiBrowzerRuntimeModule.lastIndexOf('/')); 498 | 499 | // Read the component off the disk 500 | let polipopFileName = outgoing.path.split("/").pop(); 501 | polipopFileName = polipopFileName.split("?")[0]; 502 | fs.readFile( path.join( pathToZitiBrowzerRuntimeModule, polipopFileName ), (err, data) => { 503 | 504 | if (err) { // If we can't read the file from disk 505 | 506 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 507 | res.end(''); 508 | return; 509 | 510 | } else { // Emit the file from disk 511 | 512 | let contentType = 'application/javascript'; 513 | let hasCSS = (polipopFileName.match(/\.css/) || []).length; 514 | if ((hasCSS > 0)) { 515 | contentType = 'text/css'; 516 | } 517 | 518 | var cacheData = { 519 | status: 200, 520 | headers: common.addServerHeader({ 521 | 'Content-Type': `${contentType}`, 522 | 'X-Ziti-BrowZer-Bootstrapper': 'OpenZiti browZer Polipop', 523 | }), 524 | data: data 525 | } 526 | 527 | res.writeHead(cacheData.status, cacheData.headers); 528 | 529 | res.write(cacheData.data); // the actual file contents 530 | 531 | options.cache.set(req.url, cacheData); 532 | 533 | res.end(); 534 | return; 535 | } 536 | 537 | }); 538 | 539 | return; 540 | } 541 | 542 | // 543 | // If request is for resource related to the Ziti service worker 544 | // 545 | let swRequest = (outgoing.path.match(/\/ziti-browzer-sw/) || []).length; 546 | if ((swRequest > 0)) { 547 | options.logger.silly({message: 'Request for ziti-browzer-sw', component: 'JS', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 548 | 549 | // Locate the path to the SW distro within the build of our running instance 550 | let pathToZitiBrowzerSwModule = require.resolve('@openziti/ziti-browzer-sw'); 551 | pathToZitiBrowzerSwModule = pathToZitiBrowzerSwModule.substring(0, pathToZitiBrowzerSwModule.lastIndexOf('/')); 552 | 553 | // Read the component off the disk 554 | let swFileName = outgoing.path.split("/").pop(); 555 | swFileName = swFileName.split("?")[0]; 556 | fs.readFile( path.join( pathToZitiBrowzerSwModule, swFileName ), (err, data) => { 557 | if (err) { // If we can't read the file from disk 558 | 559 | options.logger.error({error: err}); 560 | 561 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 562 | res.end(''); 563 | return; 564 | 565 | } else { // Emit the file from disk 566 | 567 | var cacheData = { 568 | status: 200, 569 | headers: common.addServerHeader({ 570 | 'Content-Type': 'application/javascript', 571 | 'Service-Worker-Allowed': '/', 572 | 'Access-Control-Allow-Headers': 'Origin', 573 | 'X-Ziti-BrowZer-Bootstrapper': 'OpenZiti browZer Service Worker', 574 | 'Origin-Trial': `${common.getOriginTrialToken()}`, 575 | }), 576 | data: data 577 | } 578 | 579 | res.writeHead(cacheData.status, cacheData.headers); 580 | 581 | res.write(cacheData.data); // the actual file contents 582 | 583 | options.cache.set(req.url, cacheData); 584 | 585 | res.end(); 586 | return; 587 | } 588 | 589 | }); 590 | 591 | return; 592 | } 593 | 594 | // 595 | // If request is for resource related to Canny 596 | // 597 | let cannyRequest = (outgoing.path.match(/\/canny-setup.js/) || []).length; 598 | if ((cannyRequest > 0)) { 599 | options.logger.silly({message: 'Request for canny-setup.js', component: 'JS', clientIp: requestIp.getClientIp(req), method: req.method, url: req.url}); 600 | 601 | // Read the component off the disk 602 | fs.readFile( path.join( __dirname, '../../../assets/canny-setup.js' ), (err, data) => { 603 | 604 | if (err) { // If we can't read the file from disk 605 | 606 | res.writeHead(500, { 'X-Ziti-BrowZer-Bootstrapper': err.message }); 607 | res.end(''); 608 | return; 609 | 610 | } else { // Emit the file from disk 611 | 612 | var cacheData = { 613 | status: 200, 614 | headers: common.addServerHeader({ 615 | 'Content-Type': 'application/javascript', 616 | }), 617 | data: data 618 | } 619 | 620 | res.writeHead(cacheData.status, cacheData.headers); 621 | 622 | res.write(cacheData.data); // the actual file contents 623 | 624 | options.cache.set(req.url, cacheData); 625 | 626 | res.end(); 627 | return; 628 | } 629 | 630 | }); 631 | 632 | return; 633 | } 634 | 635 | 636 | let html = ` 637 | 638 | 639 | 640 | OpenZiti BrowZer Bootstrapper 641 | 642 | 643 | 644 | `; 645 | 646 | res._isHtml = true; 647 | 648 | res.writeHead( 649 | 200, common.addServerHeader({ 650 | 'Content-Type': 'text/html', 651 | })); 652 | 653 | res.write( html ); 654 | 655 | res.end(); 656 | return; 657 | 658 | } 659 | 660 | }; 661 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/ziti-request.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 events = require('events'); 18 | EventEmitter = events.EventEmitter; 19 | // const v4 = require('uuid'); 20 | const { ZitiResponse } = require('./ziti-response'); 21 | 22 | 23 | const UV_EOF = -4095; 24 | 25 | /** 26 | * Base HTTP "ZitiRequest" class. Emulates the node-core `http.ClientRequest` class, but 27 | * does not use Socket, and instead integrates with the ziti-sdk-nodejs https_request_XXX 28 | * mechanisms. 29 | * 30 | * @api public 31 | */ 32 | 33 | class ZitiRequest extends EventEmitter { 34 | 35 | constructor(opts) { 36 | super(); 37 | 38 | // this.uuid = v4(); // debugging/tracing aid 39 | 40 | /** 41 | * The reference to the Ziti nodejs sdk 42 | */ 43 | this.ziti = opts.ziti; 44 | 45 | /** 46 | * 47 | */ 48 | this.opts = opts; 49 | 50 | /** 51 | * The underlying Ziti HTTP Request (i.e. the um_http_req_t returned from Ziti_http_request() in the ziti-sdk-NodeJS ) 52 | */ 53 | this.ziti_http_request; 54 | 55 | /** 56 | * This response is where we'll put any data returned from a um_http_req_t 57 | */ 58 | this.response = new ZitiResponse(); 59 | 60 | /** 61 | * Properties 62 | */ 63 | this._writable = true; 64 | 65 | } 66 | 67 | 68 | /** 69 | * Properties 70 | */ 71 | get writable() { 72 | return this._writable; 73 | } 74 | set writable(writable) { 75 | this._writable = writable; 76 | } 77 | 78 | 79 | /** 80 | * Initiate an HTTPS request. We do this by invoking the Ziti_http_request() function in the Ziti NodeJS-SDK. 81 | * @param {*} url 82 | * @param {*} method 83 | * @param {*} headers 84 | */ 85 | async do_Ziti_http_request(url, method, headers) { 86 | const self = this; 87 | return new Promise((resolve, reject) => { 88 | try { 89 | 90 | // console.log('TRANSMITTING: req uuid: %o \nmethod: %s \nurl: %s \nheaders: %o', this.uuid, method, url, headers); 91 | 92 | self.ziti.Ziti_http_request( 93 | url, 94 | method, 95 | headers, 96 | 97 | // on_req callback 98 | (obj) => { 99 | 100 | // console.log('on_req callback: req is: %o', obj.req); 101 | 102 | return resolve(obj.req); 103 | 104 | }, 105 | 106 | // on_resp callback 107 | (obj) => { 108 | 109 | // console.log('TRANSMITTING (on_resp callback): req uuid: %o \nobj: %0', this.uuid, obj); 110 | 111 | // Set properties 112 | this.response.headers = obj.headers; 113 | this.response.statusCode = obj.code; 114 | this.response.statusMessageCode = obj.status; 115 | 116 | // console.log('on_resp callback: req is: %o, statusCode: %o', this.ziti_http_request, this.response.statusCode); 117 | 118 | // console.log('on_resp callback: emitting resp: %o', this.response); 119 | 120 | this.emit('response', this.response); 121 | 122 | }, 123 | 124 | // on_resp_body callback 125 | (obj) => { 126 | 127 | // console.log('on_resp_body callback: req is: %o, len: %o', this.ziti_http_request, obj.len); 128 | 129 | // 130 | // REQUEST COMPLETE 131 | // 132 | if (obj.len === UV_EOF) { 133 | // console.log('REQUEST COMPLETE'); 134 | this.response._pushData(null); 135 | this.response.emit('close'); 136 | } 137 | 138 | // 139 | // ERROR 140 | // 141 | else if (obj.len < 0) { 142 | let err = this.requestException(obj.len); 143 | // console.log('on_resp_body callback: emitting error: %o', err); 144 | this.emit('error', err); 145 | } 146 | 147 | // 148 | // DATA RECEIVED 149 | // 150 | else { 151 | 152 | if (obj.body) { 153 | 154 | let buffer = Buffer.from(obj.body); 155 | 156 | // console.log('on_resp_body callback: DATA RECEIVED: body is: \n%s', buffer.toString()); 157 | 158 | this.response._pushData(buffer); 159 | 160 | } else { 161 | 162 | // console.error('on_resp_body callback: DATA RECEIVED: but body is undefined!'); 163 | 164 | } 165 | } 166 | 167 | }, 168 | ); 169 | 170 | } 171 | catch (e) { 172 | reject(e); 173 | } 174 | }); 175 | } 176 | 177 | 178 | /** 179 | * Initiate the HTTPS request 180 | */ 181 | async start() { 182 | 183 | let headersArray = []; 184 | 185 | for (var key of Object.keys(this.opts.headers)) { 186 | let hdr 187 | if (key !== 'host') { 188 | if (key === 'Cookie') { 189 | let value = ''; 190 | this.opts.headers[key].forEach(element => { 191 | if (value.length > 0) { 192 | value += ';'; 193 | } 194 | value += element; 195 | }); 196 | hdr = key + ':' + value; 197 | } else { 198 | hdr = key + ':' + this.opts.headers[key]; 199 | } 200 | headersArray.push(hdr); 201 | } 202 | } 203 | 204 | this.ziti_http_request = await this.do_Ziti_http_request( 205 | 206 | this.opts.href, 207 | this.opts.method, 208 | headersArray 209 | 210 | ).catch((e) => { 211 | console.error('Error: %o', e); 212 | }); 213 | } 214 | 215 | /** 216 | * 217 | */ 218 | end(chunk, encoding, callback) { 219 | 220 | if (typeof this.ziti_http_request !== 'undefined') { 221 | this.ziti.Ziti_http_request_end( this.ziti_http_request ); 222 | } 223 | 224 | return this; 225 | }; 226 | 227 | 228 | /** 229 | * Send a request body chunk. We do this by invoking the Ziti_http_request_data() function in the Ziti NodeJS-SDK. 230 | * @param {*} req 231 | * @param {*} buffer 232 | */ 233 | async do_Ziti_http_request_data(req, buffer) { 234 | const self = this; 235 | return new Promise((resolve, reject) => { 236 | try { 237 | 238 | if (typeof req === 'undefined') { 239 | throw new Error('req is "undefined"'); 240 | } 241 | 242 | self.ziti.Ziti_http_request_data( 243 | req, 244 | buffer, 245 | 246 | // on_req_body callback 247 | (obj) => { 248 | 249 | // 250 | // ERROR 251 | // 252 | if (obj.status < 0) { 253 | reject(this.requestException(obj.status)); 254 | } 255 | 256 | // 257 | // SUCCESSFUL TRANSMISSION 258 | // 259 | else { 260 | resolve(obj); 261 | } 262 | } 263 | ); 264 | } 265 | catch (e) { 266 | reject(e); 267 | } 268 | }); 269 | } 270 | 271 | 272 | /** 273 | * Send a request body chunk. 274 | */ 275 | async write(chunk, encoding, callback) { 276 | 277 | let buffer; 278 | 279 | if (typeof chunk === 'string' || chunk instanceof String) { 280 | buffer = Buffer.from(chunk, 'utf8'); 281 | } else if (Buffer.isBuffer(chunk)) { 282 | buffer = chunk; 283 | } else { 284 | throw new Error('chunk type of [' + typeof chunk + '] is not a supported type'); 285 | } 286 | 287 | let obj = await this.do_Ziti_http_request_data( 288 | 289 | this.ziti_http_request, 290 | buffer 291 | 292 | ).catch((e) => { 293 | this.emit('error', e); 294 | }); 295 | } 296 | 297 | 298 | /** 299 | * 300 | */ 301 | requestException(num) { 302 | const ex = new Error('HTTPS Request failed; code ['+num+']'); 303 | ex.code = 'EREQUEST'; 304 | return ex; 305 | } 306 | 307 | /** 308 | * 309 | */ 310 | abort() { 311 | } 312 | 313 | /** 314 | * 315 | */ 316 | dispose() { 317 | this.ziti = null; 318 | this.opts = null; 319 | this.ziti_http_request = null; 320 | this.response.dispose(); 321 | this.response = null; 322 | } 323 | 324 | } 325 | 326 | 327 | /** 328 | * Module exports. 329 | */ 330 | 331 | module.exports.ZitiRequest = ZitiRequest; 332 | -------------------------------------------------------------------------------- /lib/http-proxy/passes/ziti-response.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 Stream = require('stream'); 18 | const Duplex = Stream.Duplex; 19 | const Readable = Stream.Readable; 20 | 21 | /** 22 | * Base HTTP "ZitiResponse" class. Emulates the node-core `http.ClientRequest` class, but 23 | * does not use Socket, and instead integrates with the ziti-sdk-nodejs https_request_XXX 24 | * mechanisms. 25 | * 26 | * @api public 27 | */ 28 | 29 | class ZitiResponse extends Readable { 30 | 31 | constructor() { 32 | super(); 33 | 34 | /** 35 | * Properties 36 | */ 37 | this._headers; 38 | this._statusCode; 39 | this._statusMessage; 40 | } 41 | 42 | _read () { /* nop */ } 43 | 44 | 45 | /** 46 | * 47 | */ 48 | async _pushData(buffer) { 49 | 50 | this.push(buffer); 51 | 52 | } 53 | 54 | 55 | /** 56 | * Properties 57 | */ 58 | get headers() { 59 | return this._headers; 60 | } 61 | set headers(headers) { 62 | this._headers = headers; 63 | } 64 | get statusCode() { 65 | return this._statusCode; 66 | } 67 | set statusCode(statusCode) { 68 | this._statusCode = statusCode; 69 | } 70 | get statusMessage() { 71 | return this._statusMessage; 72 | } 73 | set statusMessage(statusMessage) { 74 | this._statusMessage = statusMessage; 75 | } 76 | 77 | /** 78 | * 79 | */ 80 | setTimeout(msecs, callback) { 81 | if (callback) 82 | this.on('timeout', callback); 83 | return this; 84 | } 85 | 86 | /** 87 | * 88 | */ 89 | destroy(error) { 90 | } 91 | 92 | 93 | /** 94 | * 95 | */ 96 | dispose() { 97 | } 98 | } 99 | 100 | 101 | /** 102 | * Module exports. 103 | */ 104 | 105 | module.exports.ZitiResponse = ZitiResponse; 106 | -------------------------------------------------------------------------------- /lib/inject.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 | var trumpet = require('trumpet'); 19 | var zlib = require('zlib'); 20 | var cookie = require('cookie'); 21 | var jwt = require('jsonwebtoken'); 22 | const {Base64} = require('js-base64'); 23 | const common = require('./http-proxy/common'); 24 | const env = require('./env'); 25 | const requestIp = require('request-ip'); 26 | const { v4: uuidv4 } = require('uuid'); 27 | 28 | module.exports = function injectBinary(options, reqSelectors, resSelectors, htmlOnly) { 29 | var _reqSelectors = reqSelectors || []; 30 | var _resSelectors = resSelectors || []; 31 | var _htmlOnly = (typeof htmlOnly == 'undefined') ? false : htmlOnly; 32 | 33 | function prepareRequestSelectors(req, res) { 34 | var tr = trumpet(); 35 | 36 | prepareSelectors(tr, _reqSelectors, req, res); 37 | 38 | req.on('data', function(data) { 39 | tr.write(data); 40 | }); 41 | } 42 | 43 | function prepareResponseSelectors(req, res) { 44 | var tr = trumpet(); 45 | var _write = res.write; 46 | var _end = res.end; 47 | var _writeHead = res.writeHead; 48 | var gunzip = zlib.Gunzip(); 49 | var theRequest = req; 50 | 51 | prepareSelectors(tr, _resSelectors, req, res); 52 | 53 | res.isHtml = function () { 54 | if (res._isHtml === undefined) { 55 | var contentType = res.getHeader('content-type') || ''; 56 | res._isHtml = contentType.indexOf('text/html') === 0; 57 | } 58 | 59 | return res._isHtml; 60 | } 61 | 62 | res.isRedirect = function() { 63 | var redirectRegex = /^201|30(1|2|7|8)$/; 64 | if( redirectRegex.test(res.statusCode)) { 65 | return true; 66 | } 67 | return false; 68 | } 69 | 70 | res.isGzipped = function () { 71 | if (res._isGzipped === undefined) { 72 | var encoding = res.getHeader('content-encoding') || ''; 73 | res._isGzipped = encoding.toLowerCase() === 'gzip' && res.isHtml(); 74 | } 75 | 76 | return res._isGzipped; 77 | } 78 | 79 | res.writeHead = function () { 80 | var headers = (arguments.length > 2) ? arguments[2] : arguments[1]; // writeHead supports (statusCode, headers) as well as (statusCode, statusMessage, headers) 81 | headers = headers || {}; 82 | 83 | /* Sniff out the content-type header. 84 | * If the response is HTML, we're safe to modify it. 85 | */ 86 | if (!_htmlOnly && res.isHtml()) { 87 | res.removeHeader('Content-Length'); 88 | delete headers['content-length']; 89 | } 90 | 91 | /* Sniff out the content-encoding header. 92 | * If the response is Gziped, we're have to gunzip content before and ungzip content after. 93 | */ 94 | if (res.isGzipped()) { 95 | res.removeHeader('Content-Encoding'); 96 | delete headers['content-encoding']; 97 | } 98 | 99 | // These are the circumstances under which we will inject the ziti-sdk-js 100 | if ( res.isHtml() && !res.isRedirect() && !res.isGzipped()) { 101 | 102 | var cookies = res.getHeaders()['set-cookie'] || []; 103 | 104 | if (!Array.isArray(cookies)) { 105 | var ca = []; 106 | ca.push(cookies); 107 | cookies = ca; 108 | } 109 | 110 | var zitiConfig = common.generateZitiConfigObject( '', req, options); 111 | zitiConfig = JSON.stringify(zitiConfig); 112 | zitiConfig = Base64.encode(zitiConfig); 113 | let domain; 114 | if (env('ZITI_BROWZER_BOOTSTRAPPER_WILDCARD_VHOSTS')) { 115 | domain = `${req.ziti_vhost}.${ common.trimFirstSection( env('ZITI_BROWZER_BOOTSTRAPPER_HOST') )}` 116 | } else { 117 | domain = `${req.ziti_vhost}`; 118 | } 119 | cookies.push( 120 | cookie.serialize( 121 | '__ziti-browzer-config', zitiConfig, 122 | { 123 | sameSite: true, 124 | path: '/', 125 | domain: `${domain}`, 126 | } 127 | ) 128 | ); 129 | 130 | 131 | res.setHeader('Set-Cookie', cookies ); 132 | 133 | 134 | } 135 | 136 | _writeHead.apply(res, arguments); 137 | }; 138 | 139 | res.write = function (data, encoding) { 140 | 141 | // Only run data through trumpet if we have HTML AND this is NOT a redirect. 142 | // 143 | // If this is a redirect, we expect the browser to come right back and ask 144 | // for the 'Location' the terget web server specified, so we will wait to 145 | // inject the ziti-sdk-js until all redirects have completed, and we have a 200. 146 | 147 | if ( res.isHtml() && !res.isRedirect() ) { 148 | 149 | if (res.isGzipped()) { 150 | 151 | gunzip.write(data); 152 | 153 | } else { 154 | 155 | // Perform HTML manipulation 156 | tr.write(data, encoding); 157 | 158 | } 159 | 160 | } else { 161 | 162 | // We do not manipulate redirect responses 163 | _write.apply(res, arguments); 164 | } 165 | }; 166 | 167 | tr.on('data', function (buf) { 168 | _write.call(res, buf); 169 | }); 170 | 171 | gunzip.on('data', function (buf) { 172 | tr.write(buf); 173 | }); 174 | 175 | res.end = function (data, encoding) { 176 | if (res.isGzipped()) { 177 | gunzip.end(data); 178 | } else { 179 | tr.end(data, encoding); 180 | } 181 | }; 182 | 183 | gunzip.on('end', function (data) { 184 | tr.end(data); 185 | }); 186 | 187 | tr.on('end', function () { 188 | _end.call(res); 189 | }); 190 | } 191 | 192 | function prepareSelectors(tr, selectors, req, res) { 193 | for (var i = 0; i < selectors.length; i++) { 194 | (function (callback, req, res) { 195 | var callbackInvoker = function(element) { 196 | callback(element, req, res); 197 | }; 198 | 199 | tr.selectAll(selectors[i].query, callbackInvoker); 200 | })(selectors[i].func, req, res); 201 | } 202 | } 203 | 204 | return function injectBinary(req, res, next) { 205 | var ignore = false; 206 | 207 | if (_htmlOnly) { 208 | var lowercaseUrl = req.url.toLowerCase(); 209 | 210 | if ((lowercaseUrl.indexOf('.js', req.url.length - 3) !== -1) || 211 | (lowercaseUrl.indexOf('.css', req.url.length - 4) !== -1)) { 212 | ignore = true; 213 | } 214 | } 215 | 216 | if (!ignore) { 217 | if (_reqSelectors.length) { 218 | prepareRequestSelectors(req, res); 219 | } 220 | 221 | if (_resSelectors.length) { 222 | prepareResponseSelectors(req, res); 223 | } 224 | } 225 | 226 | next(); 227 | }; 228 | }; 229 | -------------------------------------------------------------------------------- /lib/oidc/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 oidcUtil = exports; 18 | 19 | var env = require('../env'); 20 | var oidcClient = require('openid-client'); 21 | var isUndefined = require('lodash.isundefined'); 22 | 23 | 24 | var issuer; 25 | 26 | /** 27 | * getAccessToken() 28 | * 29 | */ 30 | oidcUtil.getAccessToken = async function ( logger ) { 31 | 32 | try { 33 | if (isUndefined(issuer)) { 34 | var idp_base_url = env('ZITI_BROWZER_BOOTSTRAPPER_IDP_BASE_URL') 35 | issuer = await oidcClient.Issuer.discover(idp_base_url); 36 | } 37 | } catch (e) { 38 | logger.error({message: `error attempting to access IdP [${idp_base_url}] - ${e.message}`}); 39 | return null; 40 | } 41 | 42 | const client = new issuer.Client({ 43 | client_id: env('ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_ID'), 44 | client_secret: env('ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_SECRET'), 45 | }); 46 | 47 | const tokenSet = await client.grant({ 48 | audience: env('ZITI_BROWZER_BOOTSTRAPPER_IDP_CLIENT_AUDIENCE'), 49 | grant_type: 'client_credentials' 50 | }); 51 | 52 | return tokenSet.access_token; 53 | } 54 | -------------------------------------------------------------------------------- /lib/terminate.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 heapdump = require('heapdump'); 18 | 19 | 20 | function terminate (server, options = { coredump: false, timeout: 500 }) { 21 | 22 | options.logger.info('terminate.options is: %o', { coredump: options.coredump, timeout: options.timeout }); 23 | 24 | // Exit function 25 | const exit = code => { 26 | options.coredump ? process.abort() : process.exit(code) 27 | } 28 | 29 | return (code, reason) => (err, promise) => { 30 | 31 | // heapdump.writeSnapshot(function(err, filename) { 32 | // console.log('dump written to', filename); 33 | // }); 34 | 35 | 36 | options.logger.error('Terminate code: %o, Reason: %o', code, reason); 37 | 38 | if (err && err instanceof Error) { 39 | if (options.logger) { 40 | options.logger.error(err.message, err.stack) 41 | } 42 | } 43 | 44 | // Attempt a graceful shutdown 45 | server.close(exit) 46 | setTimeout(exit, options.timeout).unref() 47 | } 48 | 49 | } 50 | 51 | module.exports = terminate 52 | -------------------------------------------------------------------------------- /lib/white-list-filter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 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 IPCIDR = require("ip-cidr"); 18 | const forEach = require('lodash.foreach'); 19 | 20 | 21 | module.exports = function (options) { 22 | 23 | if (!options){ 24 | options = {}; 25 | } 26 | 27 | logger = options.logger; 28 | cidrList = options.cidrList || []; 29 | whitelist = []; 30 | 31 | forEach(cidrList, function( address ) { 32 | 33 | if(!IPCIDR.isValidAddress( address )) { 34 | 35 | logger.error('whiteListFilter: invalid whitelist CIDR block specified: %o', address); 36 | process.exit(-1); 37 | 38 | } 39 | else { 40 | 41 | const cidr = new IPCIDR(address); 42 | 43 | whitelist = whitelist.concat( cidr.toArray() ); 44 | 45 | } 46 | 47 | }); 48 | 49 | logger.info('whiteListFilter: whitelist: %o', whitelist); 50 | 51 | return middleware; 52 | }; 53 | 54 | function middleware (req, res, next) { 55 | 56 | // If we were not configured with a white list, then allow request to proceed. 57 | if (whitelist.length === 0) { 58 | next(); 59 | return; 60 | } 61 | 62 | var clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress; 63 | 64 | // If client is in the white list, then allow request to proceed 65 | if (ok(clientIP)) { 66 | next(); 67 | } 68 | // Otherwise, kill it 69 | else { 70 | res.setHeader('Content-Type', 'application/json'); 71 | res.statusCode = 511; 72 | res.end('{"error":"Network Authentication Required."}'); 73 | } 74 | } 75 | 76 | 77 | function ok (clientIP) { 78 | if (whitelist.indexOf(clientIP) > -1) { 79 | return true; 80 | } else { 81 | return false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ziti-browzer-bootstrapper", 3 | "version": "0.89.0", 4 | "compatibleControllerVersion": ">=0.27.9", 5 | "description": "Ziti BrowZer Bootstrapper -- providing Ziti network access into Dark web server", 6 | "main": "index.js", 7 | "scripts": { 8 | "mocha": "mocha test/*-test.js", 9 | "test": "nyc --reporter=text --reporter=lcov npm run mocha" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/openziti/ziti-browzer-bootstrapper.git" 14 | }, 15 | "author": { 16 | "name": "NetFoundry", 17 | "url": "http://netfoundry.io", 18 | "email": "openziti@netfoundry.io" 19 | }, 20 | "license": "Apache-2.0", 21 | "dependencies": { 22 | "@openziti/ziti-browzer-edge-client": "^0.6.2", 23 | "@openziti/ziti-browzer-runtime": "^0.118.0", 24 | "@openziti/ziti-browzer-sw": "^0.83.0", 25 | "acme-http-01-standalone": "^3.0.5", 26 | "ajv": "^8.17.1", 27 | "ajv-formats": "^3.0.1", 28 | "compare-versions": "^6.0.0-rc.1", 29 | "connect": "^3.7.0", 30 | "cookie": "^1.0.2", 31 | "cookie-parser": "^1.4.6", 32 | "dotenv": "^16.4.5", 33 | "eventemitter3": "^4.0.0", 34 | "express": "^4.19.2", 35 | "express-healthcheck": "^0.1.0", 36 | "express-openid-connect": "^2.17.1", 37 | "follow-redirects": "^1.15.6", 38 | "he": "^1.2.0", 39 | "helmet": "^5.1.0", 40 | "http-error-pages": "^3.1.0", 41 | "http-proxy": "^1.18.1", 42 | "ip-cidr": "^4.0.1", 43 | "js-base64": "^3.7.7", 44 | "jsonschema": "^1.4.1", 45 | "jsonwebtoken": "^9.0.0", 46 | "le-challenge-fs": "^2.0.9", 47 | "le-store-certbot": "^2.2.3", 48 | "lodash.find": "^4.6.0", 49 | "lodash.foreach": "^4.5.0", 50 | "lodash.isequal": "^4.5.0", 51 | "lodash.isnull": "^3.0.0", 52 | "lodash.isundefined": "^3.0.1", 53 | "lodash.result": "^4.5.2", 54 | "mustache": "^4.2.0", 55 | "nconf": "^0.12.1", 56 | "nconf-validator": "^0.0.3", 57 | "node-cache": "^5.1.2", 58 | "node-cron": "^3.0.3", 59 | "node-fetch": "^3.3.2", 60 | "node_extra_ca_certs_mozilla_bundle": "^1.0.6", 61 | "openid-client": "^5.6.5", 62 | "pump": "^3.0.0", 63 | "request-ip": "^3.3.0", 64 | "requires-port": "^1.0.0", 65 | "serve-favicon": "^2.5.0", 66 | "trumpet": "^1.7.2", 67 | "urlon": "^3.1.0", 68 | "uuid": "^11.1.0", 69 | "vhost": "^3.0.2", 70 | "winston": "~3.13.0" 71 | }, 72 | "devDependencies": { 73 | "async": "^3.2.5", 74 | "auto-changelog": "^2.2.1", 75 | "concat-stream": "^2.0.0", 76 | "expect.js": "~0.3.1", 77 | "mocha": "^10.0.0", 78 | "nyc": "^15.1.0", 79 | "semver": "^7.3.7", 80 | "sse": "0.0.8", 81 | "xmlhttprequest-ssl": ">=1.6.2" 82 | }, 83 | "packageManager": "yarn@4.0.2+sha512.4e502bea682e7d8004561f916f1da2dfbe6f718024f6aa50bf8cd86f38ea3a94a7f1bf854a9ca666dd8eafcfb8d44baaa91bf5c7876e79a7aeac952c332f0e88" 84 | } 85 | -------------------------------------------------------------------------------- /zha-docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat <<-EOF 4 | {"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")", "level": "info", "message": "ZITI_BROWZER_BOOTSTRAPPER_LOG_PATH is ${ZITI_BROWZER_BOOTSTRAPPER_LOG_PATH:-null}"} 5 | EOF 6 | 7 | if [ -z ${ZITI_BROWZER_BOOTSTRAPPER_LOG_PATH} ]; then 8 | exec node index.js 9 | else 10 | exec node index.js >> "${ZITI_BROWZER_BOOTSTRAPPER_LOG_PATH}" 2>&1 11 | fi --------------------------------------------------------------------------------